From 725fe51a38d82da748b69c227113e0819c6fa0ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=A0=CE=B1=CE=BD=CE=B1=CE=B3=CE=B9=CF=8E=CF=84=CE=B7?= =?UTF-8?q?=CF=82=20=CE=93=CE=B5=CF=89=CF=81=CE=B3=CE=B1=CE=BA=CF=8C=CF=80?= =?UTF-8?q?=CE=BF=CF=85=CE=BB=CE=BF=CF=82?= Date: Wed, 17 May 2023 17:56:14 +0300 Subject: [PATCH 01/52] fix: ensure recent browser without eval (#2566) --- frontend/warn_old_browsers.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/warn_old_browsers.js b/frontend/warn_old_browsers.js index 2210694be7..cce168f5bf 100644 --- a/frontend/warn_old_browsers.js +++ b/frontend/warn_old_browsers.js @@ -3,9 +3,12 @@ function ismodern() { // See: https://kangax.github.io/compat-table/es2016plus/ // 2020 check: - return eval("let {a, ...r} = {a:1,b:1}; r?.a != r.b; 1 ?? 2") + // return eval("let {a, ...r} = {a:1,b:1}; r?.a != r.b; 1 ?? 2") // 2021 check: // return eval("let {a, ...r} = {a:1,b:1}; r?.a != r.b; 1 ?? 2; a ||= false") + // 2021 check (Chrome 85+, Firefox 77+, Safari 13.1+) + // Please check with macs + return Boolean(String.prototype.replaceAll) } catch (ex) { return false } From f691a9095cc1cf054d4b2f85bd42b48814e104ed Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Wed, 17 May 2023 17:14:30 +0200 Subject: [PATCH 02/52] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 29c146fc70..9d4bb599ef 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.25" +version = "0.19.26" [deps] Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" From a23d47732f2073fb55e7411bff9a2205b765d3c4 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Wed, 17 May 2023 18:07:25 +0200 Subject: [PATCH 03/52] ci tweaks --- .github/workflows/Bundle.yml | 2 +- test/frontend/helpers/common.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Bundle.yml b/.github/workflows/Bundle.yml index 665ead2577..ff2c46bcec 100644 --- a/.github/workflows/Bundle.yml +++ b/.github/workflows/Bundle.yml @@ -13,7 +13,7 @@ on: - release concurrency: - group: bundle + group: bundle-${{ github.ref }} cancel-in-progress: false jobs: diff --git a/test/frontend/helpers/common.js b/test/frontend/helpers/common.js index 49f894c06f..572d03e33b 100644 --- a/test/frontend/helpers/common.js +++ b/test/frontend/helpers/common.js @@ -198,7 +198,7 @@ export const createPage = async (browser) => { page.on("request", (request) => { if(blocked_domains.some(domain => request.url().includes(domain))) { if(!hide_warning(request.url())) - console.error(`Blocking request to ${request.url()}`) + console.info(`Blocking request to ${request.url()}`) request.abort(); } else { request.continue(); From 20515dd46678a49ca90e042fcfa3eab1e5c8e162 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Tue, 23 May 2023 10:02:18 +0000 Subject: [PATCH 04/52] Fix #2567 After this change, an authenticated request, which requires auth (like /open) will set the secret cookie. This means that if a first request is authenticated with a URL param, then the second request will be authenticated using the cookie --- src/webserver/Authentication.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/webserver/Authentication.jl b/src/webserver/Authentication.jl index f74b4b7d68..45644703d5 100644 --- a/src/webserver/Authentication.jl +++ b/src/webserver/Authentication.jl @@ -103,8 +103,7 @@ function auth_middleware(handler) filter!(p -> p[1] != "Access-Control-Allow-Origin", response.headers) HTTP.setheader(response, "Access-Control-Allow-Origin" => "*") end - - if HTTP.URI(request.target).path ∈ ("", "/") + if required || HTTP.URI(request.target).path ∈ ("", "/") add_set_secret_cookie!(session, response) end response From 4d9596bfa6ce01559154687da76fa5945608c9b9 Mon Sep 17 00:00:00 2001 From: Connor Burns Date: Wed, 24 May 2023 12:24:49 -0600 Subject: [PATCH 05/52] Page parameter `pluto_server_url` overrides default WebSocket address (#2570) --- frontend/components/Editor.js | 6 +++--- frontend/components/welcome/Welcome.js | 4 +++- frontend/index.js | 3 +++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/frontend/components/Editor.js b/frontend/components/Editor.js index b4dfa977d2..e6f4a95c81 100644 --- a/frontend/components/Editor.js +++ b/frontend/components/Editor.js @@ -4,7 +4,7 @@ import immer, { applyPatches, produceWithPatches } from "../imports/immer.js" import _ from "../imports/lodash.js" import { empty_notebook_state, set_disable_ui_css } from "../editor.js" -import { create_pluto_connection } from "../common/PlutoConnection.js" +import { create_pluto_connection, ws_address_from_base } from "../common/PlutoConnection.js" import { init_feedback } from "../common/Feedback.js" import { serialize_cells, deserialize_cells, detect_deserializer } from "../common/Serialization.js" @@ -851,7 +851,7 @@ patch: ${JSON.stringify( /** @type {import('../common/PlutoConnection').PlutoConnection} */ this.client = /** @type {import('../common/PlutoConnection').PlutoConnection} */ ({}) - this.connect = (ws_address = undefined) => + this.connect = (/** @type {string | undefined} */ ws_address = undefined) => create_pluto_connection({ ws_address: ws_address, on_unrequested_update: on_update, @@ -1301,7 +1301,7 @@ patch: ${JSON.stringify( count_stat(`article-view`) } } else { - this.connect() + this.connect(this.props.launch_params.pluto_server_url ? ws_address_from_base(this.props.launch_params.pluto_server_url) : undefined) } } diff --git a/frontend/components/welcome/Welcome.js b/frontend/components/welcome/Welcome.js index f52fc7aeb3..aca858f838 100644 --- a/frontend/components/welcome/Welcome.js +++ b/frontend/components/welcome/Welcome.js @@ -2,7 +2,7 @@ import _ from "../../imports/lodash.js" import { html, useEffect, useState, useRef } from "../../imports/Preact.js" import * as preact from "../../imports/Preact.js" -import { create_pluto_connection } from "../../common/PlutoConnection.js" +import { create_pluto_connection, ws_address_from_base } from "../../common/PlutoConnection.js" import { new_update_message } from "../../common/NewUpdateMessage.js" import { Open } from "./Open.js" import { Recent } from "./Recent.js" @@ -26,6 +26,7 @@ import default_featured_sources from "../../featured_sources.js" /** * @typedef LaunchParameters * @type {{ + * pluto_server_url: string?, * featured_direct_html_links: boolean, * featured_sources: import("./Featured.js").FeaturedSource[]?, * featured_source_url?: string, @@ -67,6 +68,7 @@ export const Welcome = ({ launch_params }) => { on_unrequested_update: on_update, on_connection_status: on_connection_status, on_reconnect: () => true, + ws_address: launch_params.pluto_server_url ? ws_address_from_base(launch_params.pluto_server_url) : undefined, }) client_promise.then(async (client) => { Object.assign(client_ref.current, client) diff --git a/frontend/index.js b/frontend/index.js index 1c918a2115..fc485b3e05 100644 --- a/frontend/index.js +++ b/frontend/index.js @@ -22,6 +22,9 @@ const launch_params = { featured_source_url: url_params.get("featured_source_url") ?? window.pluto_featured_source_url, //@ts-ignore featured_source_integrity: url_params.get("featured_source_integrity") ?? window.pluto_featured_source_integrity, + + //@ts-ignore + pluto_server_url: url_params.get("pluto_server_url") ?? window.pluto_server_url, } console.log("Launch parameters: ", launch_params) From 9d7e050bdbbd07958d7fe1e252968649a25bc37f Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Mon, 5 Jun 2023 16:35:33 +0200 Subject: [PATCH 06/52] css tweak --- frontend/editor.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/editor.css b/frontend/editor.css index 61c9e8fded..3d15fa28c4 100644 --- a/frontend/editor.css +++ b/frontend/editor.css @@ -452,6 +452,11 @@ pluto-output mjx-assistive-mml { height: 1px; } +/* Avoid scrollbar in cells that contain just a h3 element, like the h3 headers in https://computationalthinking.mit.edu/Fall22/images_abstractions/transformations_and_autodiff/ */ +.raw-html-wrapper > div.markdown { + overflow: hidden; +} + /* HEADER */ header#pluto-nav { From 280827087ff89be6fb8fdd28cb75d4e21d4b257e Mon Sep 17 00:00:00 2001 From: Paul Berg Date: Tue, 27 Jun 2023 22:24:46 +0200 Subject: [PATCH 07/52] Pin typescript to 5.0.4 in workflow (#2590) --- .github/workflows/TypeScriptCheck.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/TypeScriptCheck.yml b/.github/workflows/TypeScriptCheck.yml index d4830bc6c8..bf8b115bc2 100644 --- a/.github/workflows/TypeScriptCheck.yml +++ b/.github/workflows/TypeScriptCheck.yml @@ -26,7 +26,7 @@ jobs: with: node-version: "18.x" - - run: npm install typescript -g + - run: npm install typescript@5.0.4 -g - run: npm install working-directory: frontend From 1e3ee42592dfe4bd39ce9401b6e79c4011a06b72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=A0=CE=B1=CE=BD=CE=B1=CE=B3=CE=B9=CF=8E=CF=84=CE=B7?= =?UTF-8?q?=CF=82=20=CE=93=CE=B5=CF=89=CF=81=CE=B3=CE=B1=CE=BA=CF=8C=CF=80?= =?UTF-8?q?=CE=BF=CF=85=CE=BB=CE=BF=CF=82?= Date: Wed, 12 Jul 2023 19:20:08 +0300 Subject: [PATCH 08/52] fix: cryptic error on some sysimage builds on Julia 1.8.5 (#2600) --- src/webserver/Dynamic.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webserver/Dynamic.jl b/src/webserver/Dynamic.jl index 71d29f189f..8fcfe2a21d 100644 --- a/src/webserver/Dynamic.jl +++ b/src/webserver/Dynamic.jl @@ -186,7 +186,7 @@ function send_notebook_changes!(🙋::ClientRequest; commentary::Any=nothing, sk if client.connected_notebook !== nothing && client.connected_notebook.notebook_id == 🙋.notebook.notebook_id current_dict = get(current_state_for_clients, client, :empty) patches = Firebasey.diff(current_dict, notebook_dict) - patches_as_dicts::Array{Dict} = Firebasey._convert(Array{Dict}, patches) + patches_as_dicts = Firebasey._convert(Vector{Dict}, patches) current_state_for_clients[client] = deep_enough_copy(notebook_dict) # Make sure we do send a confirmation to the client who made the request, even without changes From fe236a0a4698f9d4dfb0e238693c6004a4419604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CE=A0=CE=B1=CE=BD=CE=B1=CE=B3=CE=B9=CF=8E=CF=84=CE=B7?= =?UTF-8?q?=CF=82=20=CE=93=CE=B5=CF=89=CF=81=CE=B3=CE=B1=CE=BA=CF=8C=CF=80?= =?UTF-8?q?=CE=BF=CF=85=CE=BB=CE=BF=CF=82?= Date: Wed, 12 Jul 2023 19:23:37 +0300 Subject: [PATCH 09/52] Update Project.toml (#2603) --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 9d4bb599ef..7c7f655297 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.26" +version = "0.19.27" [deps] Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" From a5c322775a765dcdbe5e27584ae16d10149aba29 Mon Sep 17 00:00:00 2001 From: Connor Burns Date: Sun, 16 Jul 2023 11:17:32 -0600 Subject: [PATCH 10/52] =?UTF-8?q?=F0=9F=9A=A6Slider=20server=20running=20s?= =?UTF-8?q?tatus=20(#2601)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/common/SliderServerClient.js | 47 ++++++++++++++++++++++++++- frontend/components/Editor.js | 5 +++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/frontend/common/SliderServerClient.js b/frontend/common/SliderServerClient.js index 49e64ffb19..e2a8933619 100644 --- a/frontend/common/SliderServerClient.js +++ b/frontend/common/SliderServerClient.js @@ -8,6 +8,23 @@ const assert_response_ok = (/** @type {Response} */ r) => (r.ok ? r : Promise.re const actions_to_keep = ["get_published_object"] +const get_start = (graph, v) => Object.values(graph).find((node) => Object.keys(node.downstream_cells_map).includes(v))?.cell_id +const get_starts = (graph, vars) => new Set([...vars].map((v) => get_start(graph, v))) +const recursive_dependencies = (graph, starts) => { + const deps = new Set(starts) + const ends = [...starts] + while (ends.length > 0) { + const node = ends.splice(0, 1)[0] + _.flatten(Object.values(graph[node].downstream_cells_map)).forEach((child) => { + if (!deps.has(child)) { + ends.push(child) + deps.add(child) + } + }) + } + return deps +} + export const nothing_actions = ({ actions }) => Object.fromEntries( Object.entries(actions).map(([k, v]) => [ @@ -23,6 +40,12 @@ export const nothing_actions = ({ actions }) => ) export const slider_server_actions = ({ setStatePromise, launch_params, actions, get_original_state, get_current_state, apply_notebook_patches }) => { + setStatePromise( + immer((state) => { + state.slider_server.connecting = true + }) + ) + const notebookfile_hash = fetch(new Request(launch_params.notebookfile, { integrity: launch_params.notebookfile_integrity })) .then(assert_response_ok) .then((r) => r.arrayBuffer()) @@ -36,7 +59,15 @@ export const slider_server_actions = ({ setStatePromise, launch_params, actions, .then((r) => r.arrayBuffer()) .then((b) => unpack(new Uint8Array(b))) - bond_connections.then((x) => console.log("Bond connections:", x)) + bond_connections.then((x) => { + console.log("Bond connections:", x) + setStatePromise( + immer((state) => { + state.slider_server.connecting = false + state.slider_server.interactive = Object.keys(x).length > 0 + }) + ) + }) const mybonds = {} const bonds_to_set = { @@ -47,6 +78,16 @@ export const slider_server_actions = ({ setStatePromise, launch_params, actions, const hash = await notebookfile_hash const graph = await bond_connections + // compute dependencies and update cell running statuses + const dep_graph = get_current_state().cell_dependencies + const starts = get_starts(dep_graph, bonds_to_set.current) + const running_cells = [...recursive_dependencies(dep_graph, starts)] + await setStatePromise( + immer((state) => { + running_cells.forEach((cell_id) => (state.notebook.cell_results[cell_id][starts.has(cell_id) ? "running" : "queued"] = true)) + }) + ) + if (bonds_to_set.current.size > 0) { const to_send = new Set(bonds_to_set.current) bonds_to_set.current.forEach((varname) => (graph[varname] ?? []).forEach((x) => to_send.add(x))) @@ -88,6 +129,10 @@ export const slider_server_actions = ({ setStatePromise, launch_params, actions, ids_of_cells_that_ran.forEach((id) => { state.cell_results[id] = original.cell_results[id] }) + running_cells.forEach((id) => { + state.cell_results[id].queued = false + state.cell_results[id].running = false + }) })(get_current_state()) ) } catch (e) { diff --git a/frontend/components/Editor.js b/frontend/components/Editor.js index e6f4a95c81..85288ab561 100644 --- a/frontend/components/Editor.js +++ b/frontend/components/Editor.js @@ -336,6 +336,11 @@ export class Editor extends Component { is_recording: false, recording_waiting_to_start: false, + + slider_server: { + connecting: false, + interactive: false, + }, } this.setStatePromise = (fn) => new Promise((r) => this.setState(fn, r)) From f76efd431f3739d8cbb37362bb9ae2b187c9a060 Mon Sep 17 00:00:00 2001 From: Paul Berg Date: Sun, 16 Jul 2023 19:18:37 +0200 Subject: [PATCH 11/52] ExEx: Track function calls in assignment lhs. (#2604) --- src/analysis/ExpressionExplorer.jl | 13 +++++++------ test/ExpressionExplorer.jl | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/analysis/ExpressionExplorer.jl b/src/analysis/ExpressionExplorer.jl index 50b773d969..1fd4b5837c 100644 --- a/src/analysis/ExpressionExplorer.jl +++ b/src/analysis/ExpressionExplorer.jl @@ -146,7 +146,7 @@ function get_assignees(ex::Expr)::FunctionName # e.g. (x, y) in the ex (x, y) = (1, 23) args = ex.args end - union!(Symbol[], Iterators.map(get_assignees, args)...) + mapfoldl(get_assignees, union!, args; init=Symbol[]) # filter(s->s isa Symbol, ex.args) elseif ex.head == :(::) # TODO: type is referenced @@ -156,7 +156,7 @@ function get_assignees(ex::Expr)::FunctionName elseif ex.head == :... # Handles splat assignments. e.g. _, y... = 1:5 args = ex.args - union!(Symbol[], Iterators.map(get_assignees, args)...) + mapfoldl(get_assignees, union!, args; init=Symbol[]) else @warn "unknown use of `=`. Assignee is unrecognised." ex Symbol[] @@ -373,12 +373,13 @@ function explore_assignment!(ex::Expr, scopestate::ScopeState)::SymbolsState global_assignees = get_global_assignees(assignees, scopestate) # If we are _not_ assigning a global variable, then this symbol hides any global definition with that name - push!(scopestate.hiddenglobals, setdiff(assignees, global_assignees)...) + union!(scopestate.hiddenglobals, setdiff(assignees, global_assignees)) assigneesymstate = explore!(ex.args[1], scopestate) - push!(scopestate.hiddenglobals, global_assignees...) - push!(symstate.assignments, global_assignees...) - push!(symstate.references, setdiff(assigneesymstate.references, global_assignees)...) + union!(scopestate.hiddenglobals, global_assignees) + union!(symstate.assignments, global_assignees) + union!(symstate.references, setdiff(assigneesymstate.references, global_assignees)) + union!(symstate.funccalls, filter!(call -> length(call) != 1 || only(call) ∉ global_assignees, assigneesymstate.funccalls)) filter!(!all_underscores, symstate.references) # Never record _ as a reference return symstate diff --git a/test/ExpressionExplorer.jl b/test/ExpressionExplorer.jl index 505b7470ea..afe010c1ff 100644 --- a/test/ExpressionExplorer.jl +++ b/test/ExpressionExplorer.jl @@ -165,6 +165,9 @@ Some of these @test_broken lines are commented out to prevent printing to the te @test testee(:(x = let a = 1; a += b end), [:b], [:x], [:+], []) @test testee(:(_ = a + 1), [:a], [], [:+], []) @test testee(:(a = _ + 1), [], [:a], [:+], []) + + @test testee(:(f()[] = 1), [], [], [:f], []) + @test testee(:(x[f()] = 1), [:x], [], [:f], []) end @testset "Multiple assignments" begin # Note that using the shorthand syntax :(a = 1, b = 2) to create an expression @@ -206,6 +209,18 @@ Some of these @test_broken lines are commented out to prevent printing to the te @test testee(quote a, b... = 0:5 end, [],[:a, :b], [[:(:)]], []) + @test testee(quote + a[x], x = 1, 2 + end, [:a], [:x], [], []) + @test testee(quote + x, a[x] = 1, 2 + end, [:a], [:x], [], []) + @test testee(quote + f, a[f()] = g + end, [:g, :a], [:f], [], []) + @test testee(quote + a[f()], f = g + end, [:g, :a], [:f], [], []) @test testee(quote (; a, b) = x end, [:x], [:a, :b], [], []) @test testee(quote a = (b, c) end, [:b, :c], [:a], [], []) From c569adf679810ca09e20810a43ffb26f1855afba Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Mon, 17 Jul 2023 10:19:08 +0200 Subject: [PATCH 12/52] =?UTF-8?q?=F0=9F=92=81=F0=9F=93=A9=20Support=20publ?= =?UTF-8?q?ish=5Fto=5Fjs=20inside=20logs=20(#2607)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/Cell.js | 4 ++- src/evaluation/WorkspaceManager.jl | 4 +++ src/runner/PlutoRunner.jl | 50 ++++++++++++++++++++---------- src/webserver/Dynamic.jl | 2 +- test/Dynamic.jl | 8 +++-- 5 files changed, 46 insertions(+), 22 deletions(-) diff --git a/frontend/components/Cell.js b/frontend/components/Cell.js index ea30b29e07..247740152a 100644 --- a/frontend/components/Cell.js +++ b/frontend/components/Cell.js @@ -311,7 +311,9 @@ export const Cell = ({ set_cm_highlighted_line=${set_cm_highlighted_line} onerror=${remount} /> - ${show_logs ? html`<${Logs} logs=${Object.values(logs)} line_heights=${line_heights} set_cm_highlighted_line=${set_cm_highlighted_line} />` : null} + ${show_logs && cell_api_ready + ? html`<${Logs} logs=${Object.values(logs)} line_heights=${line_heights} set_cm_highlighted_line=${set_cm_highlighted_line} />` + : null} <${RunArea} cell_id=${cell_id} running_disabled=${running_disabled} diff --git a/src/evaluation/WorkspaceManager.jl b/src/evaluation/WorkspaceManager.jl index 9412903f4a..55fb090655 100644 --- a/src/evaluation/WorkspaceManager.jl +++ b/src/evaluation/WorkspaceManager.jl @@ -229,6 +229,10 @@ function start_relaying_logs((session, notebook)::SN, log_channel::Distributed.R @assert !isnothing(display_cell) + # this handles the use of published_to_js inside logs: objects that were newly published during the rendering of the log args. + merge!(display_cell.published_objects, next_log["new_published_objects"]) + delete!(next_log, "new_published_objects") + push!(display_cell.logs, next_log) Pluto.@asynclog update_throttled() catch e diff --git a/src/runner/PlutoRunner.jl b/src/runner/PlutoRunner.jl index 5e3b49e81f..8ff95058ac 100644 --- a/src/runner/PlutoRunner.jl +++ b/src/runner/PlutoRunner.jl @@ -2125,28 +2125,26 @@ end""" """ const currently_running_cell_id = Ref{UUID}(uuid4()) -function _publish(x, id_start)::String - assertpackable(x) - - id = string(notebook_id[], "/", currently_running_cell_id[], "/", id_start) - d = get!(Dict{String,Any}, cell_published_objects, currently_running_cell_id[]) +function _publish(x, id_start, cell_id)::String + id = "$(notebook_id[])/$cell_id/$id_start" + d = get!(Dict{String,Any}, cell_published_objects, cell_id) d[id] = x return id end -_publish(x) = _publish(x, objectid2str(x)) - # TODO? Possibly move this to it's own package, with fallback that actually msgpack? # ..... Ideally we'd make this require `await` on the javascript side too... Base.@kwdef struct PublishedToJavascript - published_id + published_object + published_id_start cell_id end function Base.show(io::IO, ::MIME"text/javascript", published::PublishedToJavascript) - if published.cell_id != currently_running_cell_id[] - error("Showing result from PlutoRunner.publish_to_js() in a cell different from where it was created, not (yet?) supported.") - end - write(io, "/* See the documentation for PlutoRunner.publish_to_js */ getPublishedObject(\"$(published.published_id)\")") + id = _publish(published.published_object, published.published_id_start, published.cell_id) + # if published.cell_id != currently_running_cell_id[] + # error("Showing result from PlutoRunner.publish_to_js() in a cell different from where it was created, not (yet?) supported.") + # end + write(io, "/* See the documentation for PlutoRunner.publish_to_js */ getPublishedObject(\"$(id)\")") end Base.show(io::IO, ::MIME"text/plain", published::PublishedToJavascript) = show(io, MIME("text/javascript"), published) Base.show(io::IO, published::PublishedToJavascript) = show(io, MIME("text/javascript"), published) @@ -2179,9 +2177,13 @@ let end ``` """ -function publish_to_js(args...) +publish_to_js(x) = publish_to_js(x, objectid2str(x)) + +function publish_to_js(x, id_start) + assertpackable(x) PublishedToJavascript( - published_id=_publish(args...), + published_object=x, + published_id_start=id_start, cell_id=currently_running_cell_id[], ) end @@ -2387,18 +2389,32 @@ function Logging.handle_message(pl::PlutoCellLogger, level, msg, _module, group, end try - yield() + po() = get(cell_published_objects, pl.cell_id, Dict{String,Any}()) + before_published_object_keys = collect(keys(po())) + + # Render the log arguments: + msg_formatted = format_output_default(msg isa AbstractString ? Text(msg) : msg) + kwargs_formatted = Tuple{String,Any}[(string(k), format_log_value(v)) for (k, v) in kwargs if k != :maxlog] + + after_published_object_keys = collect(keys(po())) + new_published_object_keys = setdiff(after_published_object_keys, before_published_object_keys) + + # (Running `put!(pl.log_channel, x)` will send `x` to the pluto server. See `start_relaying_logs` for the receiving end.) put!(pl.log_channel, Dict{String,Any}( "level" => string(level), - "msg" => format_output_default(msg isa AbstractString ? Text(msg) : msg), + "msg" => msg_formatted, + # This is a dictionary containing all published objects that were published during the rendering of the log arguments (we cannot track which objects were published during the execution of the log statement itself i think...) + "new_published_objects" => Dict{String,Any}( + key => po()[key] for key in new_published_object_keys + ), "group" => string(group), "id" => string(id), "file" => string(file), "cell_id" => pl.cell_id, "line" => line isa Union{Int32,Int64} ? line : nothing, - "kwargs" => Tuple{String,Any}[(string(k), format_log_value(v)) for (k, v) in kwargs if k != :maxlog], + "kwargs" => kwargs_formatted, )) yield() diff --git a/src/webserver/Dynamic.jl b/src/webserver/Dynamic.jl index 8fcfe2a21d..60e3677510 100644 --- a/src/webserver/Dynamic.jl +++ b/src/webserver/Dynamic.jl @@ -133,7 +133,7 @@ function notebook_to_js(notebook::Notebook) "cell_id" => cell.cell_id, "depends_on_disabled_cells" => cell.depends_on_disabled_cells, "output" => FirebaseyUtils.ImmutableMarker(cell.output), - "published_object_keys" => keys(cell.published_objects), + "published_object_keys" => collect(keys(cell.published_objects)), "queued" => cell.queued, "running" => cell.running, "errored" => cell.errored, diff --git a/test/Dynamic.jl b/test/Dynamic.jl index ed0e3e12c8..f833933ff6 100644 --- a/test/Dynamic.jl +++ b/test/Dynamic.jl @@ -194,17 +194,19 @@ end @testset "PlutoRunner API" begin 🍭 = ServerSession() 🍭.options.evaluation.workspace_use_distributed = true + + cid = uuid1() notebook = Notebook([ Cell("PlutoRunner.notebook_id[] |> Text"), - Cell(""" + Cell(cid, """ let # not actually public API but we test it anyways a = PlutoRunner._publish(Dict( "hello" => "world", "xx" => UInt8[6,7,8], - )) - b = PlutoRunner._publish("cool") + ), "aaa", Base.UUID("$cid")) + b = PlutoRunner._publish("cool", "bbb", Base.UUID("$cid")) Text((a, b)) end """), From 3958cf5133c1371a559ed5f76ca955dad2bb1a48 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Mon, 17 Jul 2023 15:56:13 +0200 Subject: [PATCH 13/52] Fix #2398 --- frontend/common/PlutoConnection.js | 14 +++++--------- frontend/components/Editor.js | 27 +++++++++++++++++++++++++-- frontend/components/Popup.js | 11 ++++++++--- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/frontend/common/PlutoConnection.js b/frontend/common/PlutoConnection.js index b4c336a04d..4e6fa5660a 100644 --- a/frontend/common/PlutoConnection.js +++ b/frontend/common/PlutoConnection.js @@ -265,7 +265,7 @@ const default_ws_address = () => ws_address_from_base(window.location.href) * @param {{ * on_unrequested_update: (message: PlutoMessage, by_me: boolean) => void, * on_reconnect: () => boolean, - * on_connection_status: (connection_status: boolean) => void, + * on_connection_status: (connection_status: boolean, hopeless: boolean) => void, * connect_metadata?: Object, * ws_address?: String, * }} options @@ -364,7 +364,7 @@ export const create_pluto_connection = async ({ on_unrequested_update(update, by_me) }, on_socket_close: async () => { - on_connection_status(false) + on_connection_status(false, false) console.log(`Starting new websocket`, new Date().toLocaleTimeString()) await Promises.delay(reconnect_after_close_delay) @@ -373,7 +373,7 @@ export const create_pluto_connection = async ({ console.log(`Starting state sync`, new Date().toLocaleTimeString()) const accept = on_reconnect() console.log(`State sync ${accept ? "" : "not "}successful`, new Date().toLocaleTimeString()) - on_connection_status(accept) + on_connection_status(accept, false) if (!accept) { alert("Connection out of sync 😥\n\nRefresh the page to continue") } @@ -394,14 +394,10 @@ export const create_pluto_connection = async ({ console.log("Client object: ", client) if (connect_metadata.notebook_id != null && !u.message.notebook_exists) { - // https://github.com/fonsp/Pluto.jl/issues/55 - if (confirm("A new server was started - this notebook session is no longer running.\n\nWould you like to go back to the main menu?")) { - window.location.href = "./" - } - on_connection_status(false) + on_connection_status(false, true) return {} } - on_connection_status(true) + on_connection_status(true, false) const ping = () => { send("ping", {}, {}) diff --git a/frontend/components/Editor.js b/frontend/components/Editor.js index 85288ab561..5134297b64 100644 --- a/frontend/components/Editor.js +++ b/frontend/components/Editor.js @@ -18,7 +18,7 @@ import { RecentlyDisabledInfo, UndoDelete } from "./UndoDelete.js" import { SlideControls } from "./SlideControls.js" import { Scroller } from "./Scroller.js" import { ExportBanner } from "./ExportBanner.js" -import { Popup } from "./Popup.js" +import { open_pluto_popup, Popup } from "./Popup.js" import { slice_utf8, length_utf8 } from "../common/UnicodeTools.js" import { has_ctrl_or_cmd_pressed, ctrl_or_cmd_name, is_mac_keyboard, in_textarea_or_input } from "../common/KeyboardShortcuts.js" @@ -840,7 +840,30 @@ patch: ${JSON.stringify( setTimeout(init_feedback, 2 * 1000) // 2 seconds - load feedback a little later for snappier UI } - const on_connection_status = (val) => this.setState({ connected: val }) + const on_connection_status = (val, hopeless) => { + this.setState({ connected: val }) + if (hopeless) { + // https://github.com/fonsp/Pluto.jl/issues/55 + // https://github.com/fonsp/Pluto.jl/issues/2398 + open_pluto_popup({ + type: "info", + source_element: null, + body: html`

A new server was started - this notebook session is no longer running.

+

Would you like to go back to the main menu?

+
+ Go back +
+ { + e.preventDefault() + window.dispatchEvent(new CustomEvent("close pluto popup")) + }} + >Stay here`, + }) + } + } const on_reconnect = () => { console.warn("Reconnected! Checking states") diff --git a/frontend/components/Popup.js b/frontend/components/Popup.js index 8e7a59e533..fae1c99e1a 100644 --- a/frontend/components/Popup.js +++ b/frontend/components/Popup.js @@ -48,10 +48,15 @@ export const Popup = ({ notebook, disable_input }) => { const el = e.detail.source_element recent_source_element_ref.current = el - const elb = el.getBoundingClientRect() - const bodyb = document.body.getBoundingClientRect() + if (el == null) { + pos_ref.current = `top: 20%; left: 50%; transform: translate(-50%, -50%); position: fixed;` + } else { + const elb = el.getBoundingClientRect() + const bodyb = document.body.getBoundingClientRect() + + pos_ref.current = `top: ${0.5 * (elb.top + elb.bottom) - bodyb.top}px; left: min(max(0px,100vw - 251px - 30px), ${elb.right - bodyb.left}px);` + } - pos_ref.current = `top: ${0.5 * (elb.top + elb.bottom) - bodyb.top}px; left: min(max(0px,100vw - 251px - 30px), ${elb.right - bodyb.left}px);` set_recent_event(e.detail) } From 2cc4e4468186e2cdf417249d08c46a5e65fd19b6 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Mon, 17 Jul 2023 15:56:19 +0200 Subject: [PATCH 14/52] css tweak --- frontend/editor.css | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/editor.css b/frontend/editor.css index 3d15fa28c4..52bddb302b 100644 --- a/frontend/editor.css +++ b/frontend/editor.css @@ -2327,6 +2327,7 @@ pluto-helpbox.hidden > section { } .helpbox-docs > section h1 { font-size: 1.3rem; + overflow-wrap: anywhere; } .helpbox-docs > section pre { padding: 0.7rem 0.5rem; From 7f1f27e650d29182986c573f9761d1cb8b98b57a Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Mon, 17 Jul 2023 16:56:01 +0200 Subject: [PATCH 15/52] Experimental JS API for getting/setting cell metadata (#2612) --- frontend/components/CellOutput.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/frontend/components/CellOutput.js b/frontend/components/CellOutput.js index 4335ea485b..ff4188cf36 100644 --- a/frontend/components/CellOutput.js +++ b/frontend/components/CellOutput.js @@ -382,6 +382,21 @@ const execute_scripttags = async ({ root_node, script_nodes, previous_results_ma delete notebook.metadata[key] }), + ...(cell == null + ? {} + : { + getCellMetadataExperimental: (key, { cell_id = null } = {}) => + pluto_actions.get_notebook()?.cell_inputs?.[cell_id ?? cell.id]?.metadata[key], + setCellMetadataExperimental: (key, value, { cell_id = null } = {}) => + pluto_actions.update_notebook((notebook) => { + notebook.cell_inputs[cell_id ?? cell.id].metadata[key] = value + }), + deleteCellMetadataExperimental: (key, { cell_id = null } = {}) => + pluto_actions.update_notebook((notebook) => { + delete notebook.cell_inputs[cell_id ?? cell.id].metadata[key] + }), + }), + ...observablehq_for_cells, }, code, From c80464d018ebe4ee230fac9b1619c4b3aab39cb8 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Mon, 17 Jul 2023 16:57:27 +0200 Subject: [PATCH 16/52] Experimental JS API for getting/setting cell metadata (#2612) From 0071ec12b0d98c05335159fe45da69808d354bb8 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Mon, 17 Jul 2023 18:01:04 +0200 Subject: [PATCH 17/52] popup: warn type --- frontend/components/Editor.js | 2 +- frontend/components/Popup.js | 5 +++-- frontend/editor.css | 5 +++++ frontend/light_color.css | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/frontend/components/Editor.js b/frontend/components/Editor.js index 5134297b64..50f4326a71 100644 --- a/frontend/components/Editor.js +++ b/frontend/components/Editor.js @@ -846,7 +846,7 @@ patch: ${JSON.stringify( // https://github.com/fonsp/Pluto.jl/issues/55 // https://github.com/fonsp/Pluto.jl/issues/2398 open_pluto_popup({ - type: "info", + type: "warn", source_element: null, body: html`

A new server was started - this notebook session is no longer running.

Would you like to go back to the main menu?

diff --git a/frontend/components/Popup.js b/frontend/components/Popup.js index fae1c99e1a..16d3491ac2 100644 --- a/frontend/components/Popup.js +++ b/frontend/components/Popup.js @@ -26,7 +26,7 @@ export const help_circle_icon = new URL("https://cdn.jsdelivr.net/gh/ionic-team/ /** * @typedef MiscPopupDetails - * @property {string} type + * @property {"info" | "warn"} type * @property {import("../imports/Preact.js").ReactElement} body * @property {HTMLElement?} source_element */ @@ -95,6 +95,7 @@ export const Popup = ({ notebook, disable_input }) => { return html` @@ -105,7 +106,7 @@ export const Popup = ({ notebook, disable_input }) => { recent_event=${recent_event} clear_recent_event=${() => set_recent_event(null)} />` - : type === "info" + : type === "info" || type === "warn" ? html`
${recent_event?.body}
` : null}
` diff --git a/frontend/editor.css b/frontend/editor.css index 52bddb302b..d2cbba88e3 100644 --- a/frontend/editor.css +++ b/frontend/editor.css @@ -1810,6 +1810,11 @@ pluto-popup > * { position: absolute; } +pluto-popup.warn > * { + background: var(--pluto-logs-warn-color); + border-color: var(--pluto-logs-warn-accent-color); +} + pluto-popup code.auto_disabled_variable { font-family: var(--julia-mono-font-stack); font-size: 0.8rem; diff --git a/frontend/light_color.css b/frontend/light_color.css index 6685329f48..0f7e2ce49f 100644 --- a/frontend/light_color.css +++ b/frontend/light_color.css @@ -111,7 +111,7 @@ /*saveall container*/ --overlay-button-bg: #ffffff; - --overlay-button-border: #f3f2f2; + --overlay-button-border: hsl(0 4% 91% / 1); --overlay-button-border-save: #f3f2f2; /*input_context_menu*/ From 9c052d39d47055444bc03a48abadc9134a27e951 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Mon, 17 Jul 2023 18:23:47 +0200 Subject: [PATCH 18/52] =?UTF-8?q?=F0=9F=90=9D=20Fix=20#2518?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/CellInput.js | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/frontend/components/CellInput.js b/frontend/components/CellInput.js index e3a57c6421..9133b305a2 100644 --- a/frontend/components/CellInput.js +++ b/frontend/components/CellInput.js @@ -631,17 +631,23 @@ export const CellInput = ({ // Remove selection on blur EditorView.domEventHandlers({ blur: (event, view) => { - // collapse the selection into a single point - view.dispatch({ - selection: { - anchor: view.state.selection.main.head, - }, - scrollIntoView: false, - }) - // and blur the DOM again (because the previous transaction might have re-focused it) - view.contentDOM.blur() - - set_cm_forced_focus(null) + // it turns out that this condition is true *exactly* if and only if the blur event was triggered by blurring the window + let caused_by_window_blur = document.activeElement === view.contentDOM + + if (!caused_by_window_blur) { + // then it's caused by focusing something other than this cell in the editor. + // in this case, we want to collapse the selection into a single point, for aesthetic reasons. + view.dispatch({ + selection: { + anchor: view.state.selection.main.head, + }, + scrollIntoView: false, + }) + // and blur the DOM again (because the previous transaction might have re-focused it) + view.contentDOM.blur() + + set_cm_forced_focus(null) + } }, }), pluto_paste_plugin({ From 48337a589895839869ad8d1ebcf7bf80f4f2cce1 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Thu, 20 Jul 2023 21:03:49 +0200 Subject: [PATCH 19/52] frontmatter editor: remove autocapitalizationr --- frontend/editor.css | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/editor.css b/frontend/editor.css index d2cbba88e3..c573b7c38c 100644 --- a/frontend/editor.css +++ b/frontend/editor.css @@ -3415,7 +3415,6 @@ pluto-cell.hooked_up pluto-output { } .pluto-frontmatter label { - text-transform: capitalize; font-weight: 500; } From 5dc359dd7883c585eb89b213e6538eaa25f9ecee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Jul 2023 21:34:12 +0200 Subject: [PATCH 20/52] Bump word-wrap from 1.2.3 to 1.2.4 in /frontend-bundler (#2617) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend-bundler/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend-bundler/package-lock.json b/frontend-bundler/package-lock.json index ee443a186a..56b24cf5b2 100644 --- a/frontend-bundler/package-lock.json +++ b/frontend-bundler/package-lock.json @@ -6874,9 +6874,9 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", "engines": { "node": ">=0.10.0" } @@ -11986,9 +11986,9 @@ } }, "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==" }, "wrap-ansi": { "version": "7.0.0", From 56541139902dda3890d5b0130a8ec04c35efbdad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Jul 2023 21:34:59 +0200 Subject: [PATCH 21/52] Bump terser from 5.9.0 to 5.14.2 in /frontend-bundler (#2220) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend-bundler/package-lock.json | 142 ++++++++++++++++++++++++----- 1 file changed, 121 insertions(+), 21 deletions(-) diff --git a/frontend-bundler/package-lock.json b/frontend-bundler/package-lock.json index 56b24cf5b2..7fdd990813 100644 --- a/frontend-bundler/package-lock.json +++ b/frontend-bundler/package-lock.json @@ -385,6 +385,58 @@ "node": ">=6.9.0" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", + "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@lezer/common": { "version": "0.15.12", "resolved": "https://registry.npmjs.org/@lezer/common/-/common-0.15.12.tgz", @@ -6427,12 +6479,13 @@ } }, "node_modules/terser": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.9.0.tgz", - "integrity": "sha512-h5hxa23sCdpzcye/7b8YqbE5OwKca/ni0RQz1uRX3tGh8haaGHqcuSqbGRybuAKNdntZ0mDgFNXPJ48xQ2RXKQ==", + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", + "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.7.2", "source-map-support": "~0.5.20" }, "bin": { @@ -6442,19 +6495,22 @@ "node": ">=10" } }, + "node_modules/terser/node_modules/acorn": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, - "node_modules/terser/node_modules/source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "engines": { - "node": ">= 8" - } - }, "node_modules/timers-browserify": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", @@ -7304,6 +7360,49 @@ "to-fast-properties": "^2.0.0" } }, + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" + }, + "@jridgewell/source-map": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", + "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz", + "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==", + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "@lezer/common": { "version": "0.15.12", "resolved": "https://registry.npmjs.org/@lezer/common/-/common-0.15.12.tgz", @@ -11618,24 +11717,25 @@ "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==" }, "terser": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.9.0.tgz", - "integrity": "sha512-h5hxa23sCdpzcye/7b8YqbE5OwKca/ni0RQz1uRX3tGh8haaGHqcuSqbGRybuAKNdntZ0mDgFNXPJ48xQ2RXKQ==", + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz", + "integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==", "requires": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", "commander": "^2.20.0", - "source-map": "~0.7.2", "source-map-support": "~0.5.20" }, "dependencies": { + "acorn": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==" + }, "commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" } } }, From ff88e45cb07e9b7ef1b5182040b0a7f7f8b42bea Mon Sep 17 00:00:00 2001 From: Paul Berg Date: Thu, 20 Jul 2023 12:36:01 -0700 Subject: [PATCH 22/52] Update the folded state when reloading from file (#2602) Co-authored-by: Tor Erlend Fjelde <11074788+torfjelde@users.noreply.github.com> Co-authored-by: Fons van der Plas --- src/evaluation/Run.jl | 18 ++++++++++-------- test/ReloadFromFile.jl | 13 ++++++++----- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/evaluation/Run.jl b/src/evaluation/Run.jl index 035796a8e0..b9eb7679f9 100644 --- a/src/evaluation/Run.jl +++ b/src/evaluation/Run.jl @@ -524,6 +524,7 @@ function notebook_differences(from::Notebook, to::Notebook) end end, + folded_changed = any(from_cells[id].code_folded != to_cells[id].code_folded for id in keys(from_cells) if haskey(to_cells, id)), order_changed = from.cell_order != to.cell_order, nbpkg_changed = !is_nbpkg_equal(from.nbpkg_ctx, to.nbpkg_ctx), ) @@ -555,16 +556,17 @@ function update_from_file(session::ServerSession, notebook::Notebook; kwargs...) # @show added removed changed cells_changed = !(isempty(added) && isempty(removed) && isempty(changed)) + folded_changed = d.folded_changed order_changed = d.order_changed nbpkg_changed = d.nbpkg_changed - something_changed = cells_changed || order_changed || (include_nbpg && nbpkg_changed) - + something_changed = cells_changed || folded_changed || order_changed || (include_nbpg && nbpkg_changed) + if something_changed @info "Reloading notebook from file and applying changes!" notebook.last_hot_reload_time = time() end - + for c in added notebook.cells_dict[c] = just_loaded.cells_dict[c] end @@ -579,10 +581,10 @@ function update_from_file(session::ServerSession, notebook::Notebook; kwargs...) for c in keys(notebook.cells_dict) ∩ keys(just_loaded.cells_dict) notebook.cells_dict[c].code_folded = just_loaded.cells_dict[c].code_folded end - + notebook.cell_order = just_loaded.cell_order notebook.metadata = just_loaded.metadata - + if include_nbpg && nbpkg_changed @info "nbpkgs not equal" (notebook.nbpkg_ctx isa Nothing) (just_loaded.nbpkg_ctx isa Nothing) @@ -599,11 +601,11 @@ function update_from_file(session::ServerSession, notebook::Notebook; kwargs...) end notebook.nbpkg_restart_required_msg = "Yes, because the file was changed externally and the embedded Pkg changed." end - + if something_changed - update_save_run!(session, notebook, Cell[notebook.cells_dict[c] for c in union(added, changed)]; kwargs...) # this will also update nbpkg + update_save_run!(session, notebook, Cell[notebook.cells_dict[c] for c in union(added, changed)]; kwargs...) # this will also update nbpkg if needed end - + return true end diff --git a/test/ReloadFromFile.jl b/test/ReloadFromFile.jl index 01bf9358ca..7cc6b9aac4 100644 --- a/test/ReloadFromFile.jl +++ b/test/ReloadFromFile.jl @@ -101,14 +101,14 @@ end sleep(timeout_between_tests) file4 = read(notebook.path, String) + last_hot_reload_time4 = notebook.last_hot_reload_time notebook.cells[3].code_folded = true save_notebook(notebook) sleep(timeout_between_tests) file5 = read(notebook.path, String) - @assert file4 != file5 - - @assert notebook.cells[3].code_folded + @test file4 != file5 + @test notebook.cells[3].code_folded write(notebook.path, file4) @@ -119,7 +119,10 @@ end # cell folded, but cell should not re-run @assert original_rand_output == notebook.cells[3].output.body - + @assert poll(10) do + last_hot_reload_time5 = notebook.last_hot_reload_time + last_hot_reload_time5 != last_hot_reload_time4 + end ### sleep(timeout_between_tests) @@ -160,4 +163,4 @@ end @assert notebook.nbpkg_restart_required_msg !== nothing end @test true -end \ No newline at end of file +end From 532587dde06a3953a1090e1d6490524111c7f422 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Jul 2023 21:37:52 +0200 Subject: [PATCH 23/52] Bump word-wrap from 1.2.3 to 1.2.4 in /test/frontend (#2616) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- test/frontend/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/frontend/package-lock.json b/test/frontend/package-lock.json index df0fc81029..27f3b646a0 100644 --- a/test/frontend/package-lock.json +++ b/test/frontend/package-lock.json @@ -9077,9 +9077,9 @@ "dev": true }, "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -16124,9 +16124,9 @@ "dev": true }, "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", "dev": true }, "wrap-ansi": { From c8f80f47ec751d37fc5a3a4de79ff7d58505d478 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Jul 2023 21:38:11 +0200 Subject: [PATCH 24/52] Bump semver from 5.7.1 to 5.7.2 in /frontend-bundler (#2599) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend-bundler/package-lock.json | 36 +++++++++++++++--------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/frontend-bundler/package-lock.json b/frontend-bundler/package-lock.json index 7fdd990813..a9b30f6cff 100644 --- a/frontend-bundler/package-lock.json +++ b/frontend-bundler/package-lock.json @@ -76,9 +76,9 @@ } }, "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } @@ -114,9 +114,9 @@ } }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } @@ -6148,9 +6148,9 @@ } }, "node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "bin": { "semver": "bin/semver" } @@ -7135,9 +7135,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" } } }, @@ -7163,9 +7163,9 @@ }, "dependencies": { "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" } } }, @@ -11463,9 +11463,9 @@ } }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" }, "serve-handler": { "version": "6.1.5", From c5dbeb14a655c66f6b8a650684f7bc4c6cfc2387 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Jul 2023 21:38:43 +0200 Subject: [PATCH 25/52] Bump tough-cookie from 4.0.0 to 4.1.3 in /test/frontend (#2597) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- test/frontend/package-lock.json | 74 ++++++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 14 deletions(-) diff --git a/test/frontend/package-lock.json b/test/frontend/package-lock.json index 27f3b646a0..9f056296f2 100644 --- a/test/frontend/package-lock.json +++ b/test/frontend/package-lock.json @@ -7320,6 +7320,12 @@ } } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -7519,6 +7525,12 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, "node_modules/resolve": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", @@ -8724,14 +8736,15 @@ } }, "node_modules/tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "dev": true, "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", - "universalify": "^0.1.2" + "universalify": "^0.2.0", + "url-parse": "^1.5.3" }, "engines": { "node": ">=6" @@ -8866,9 +8879,9 @@ } }, "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", "dev": true, "engines": { "node": ">= 4.0.0" @@ -8929,6 +8942,16 @@ "deprecated": "Please see https://github.com/lydell/urix#deprecated", "dev": true }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", @@ -14716,6 +14739,12 @@ } } }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -14879,6 +14908,12 @@ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, "resolve": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", @@ -15842,14 +15877,15 @@ } }, "tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "dev": true, "requires": { "psl": "^1.1.33", "punycode": "^2.1.1", - "universalify": "^0.1.2" + "universalify": "^0.2.0", + "url-parse": "^1.5.3" } }, "tr46": { @@ -15950,9 +15986,9 @@ } }, "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", "dev": true }, "unset-value": { @@ -16001,6 +16037,16 @@ "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", "dev": true }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", From 1d2c3038032108fff3a9c565eaa84609f1e75b4e Mon Sep 17 00:00:00 2001 From: Alberto Mengali Date: Thu, 20 Jul 2023 22:58:48 +0200 Subject: [PATCH 26/52] Make `published_to_js` (now `publish_to_js`) official API through AbstractPlutoDingetjes.jl (v2) (#2608) Co-authored-by: Fons van der Plas --- src/runner/PlutoRunner.jl | 109 ++++++++++----------- test/Dynamic.jl | 72 +++++++++++--- test/frontend/__tests__/published_to_js.js | 58 +++++++++++ test/frontend/fixtures/published_to_js.jl | 50 ++++++++++ 4 files changed, 216 insertions(+), 73 deletions(-) create mode 100644 test/frontend/__tests__/published_to_js.js create mode 100644 test/frontend/fixtures/published_to_js.jl diff --git a/src/runner/PlutoRunner.jl b/src/runner/PlutoRunner.jl index 8ff95058ac..6916596b7a 100644 --- a/src/runner/PlutoRunner.jl +++ b/src/runner/PlutoRunner.jl @@ -908,7 +908,13 @@ function formatted_result_of( 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 - format_output(ans; context=IOContext(default_iocontext, :extra_items=>extra_items, :module => workspace)) + format_output(ans; context=IOContext( + default_iocontext, + :extra_items=>extra_items, + :module => workspace, + :pluto_notebook_id => notebook_id, + :pluto_cell_id => cell_id, + )) end else ("", MIME"text/plain"()) @@ -972,6 +978,7 @@ const default_iocontext = IOContext(devnull, :displaysize => (18, 88), :is_pluto => true, :pluto_supported_integration_features => supported_integration_features, + :pluto_published_to_js => (io, x) -> core_published_to_js(io, x), ) const default_stdout_iocontext = IOContext(devnull, @@ -1494,17 +1501,29 @@ const integrations = Integration[ id = Base.PkgId(Base.UUID(reinterpret(UInt128, codeunits("Paul Berg Berlin")) |> first), "AbstractPlutoDingetjes"), code = quote @assert v"1.0.0" <= AbstractPlutoDingetjes.MY_VERSION < v"2.0.0" - initial_value_getter_ref[] = AbstractPlutoDingetjes.Bonds.initial_value - transform_value_ref[] = AbstractPlutoDingetjes.Bonds.transform_value - possible_bond_values_ref[] = AbstractPlutoDingetjes.Bonds.possible_values - - push!(supported_integration_features, + + supported!(xs...) = push!(supported_integration_features, xs...) + + # don't need feature checks for these because they existed in every version of AbstractPlutoDingetjes: + supported!( AbstractPlutoDingetjes, AbstractPlutoDingetjes.Bonds, AbstractPlutoDingetjes.Bonds.initial_value, AbstractPlutoDingetjes.Bonds.transform_value, AbstractPlutoDingetjes.Bonds.possible_values, ) + initial_value_getter_ref[] = AbstractPlutoDingetjes.Bonds.initial_value + transform_value_ref[] = AbstractPlutoDingetjes.Bonds.transform_value + possible_bond_values_ref[] = AbstractPlutoDingetjes.Bonds.possible_values + + # feature checks because these were added in a later release of AbstractPlutoDingetjes + if isdefined(AbstractPlutoDingetjes, :Display) + supported!(AbstractPlutoDingetjes.Display) + if isdefined(AbstractPlutoDingetjes.Display, :published_to_js) + supported!(AbstractPlutoDingetjes.Display.published_to_js) + end + end + end, ), Integration( @@ -2125,74 +2144,48 @@ end""" """ const currently_running_cell_id = Ref{UUID}(uuid4()) -function _publish(x, id_start, cell_id)::String - id = "$(notebook_id[])/$cell_id/$id_start" - d = get!(Dict{String,Any}, cell_published_objects, cell_id) +function core_published_to_js(io, x) + assertpackable(x) + + id_start = objectid2str(x) + + _notebook_id = get(io, :pluto_notebook_id, notebook_id[])::UUID + _cell_id = get(io, :pluto_cell_id, currently_running_cell_id[])::UUID + + # The unique identifier of this object + id = "$_notebook_id/$id_start" + + d = get!(Dict{String,Any}, cell_published_objects, _cell_id) d[id] = x - return id + + write(io, "/* See the documentation for AbstractPlutoDingetjes.Display.published_to_js */ getPublishedObject(\"$(id)\")") + + return nothing end -# TODO? Possibly move this to it's own package, with fallback that actually msgpack? -# ..... Ideally we'd make this require `await` on the javascript side too... -Base.@kwdef struct PublishedToJavascript +# TODO: This is the deprecated old function. Remove me at some point. +struct PublishedToJavascript published_object - published_id_start - cell_id end function Base.show(io::IO, ::MIME"text/javascript", published::PublishedToJavascript) - id = _publish(published.published_object, published.published_id_start, published.cell_id) - # if published.cell_id != currently_running_cell_id[] - # error("Showing result from PlutoRunner.publish_to_js() in a cell different from where it was created, not (yet?) supported.") - # end - write(io, "/* See the documentation for PlutoRunner.publish_to_js */ getPublishedObject(\"$(id)\")") + core_published_to_js(io, published.published_object) end Base.show(io::IO, ::MIME"text/plain", published::PublishedToJavascript) = show(io, MIME("text/javascript"), published) Base.show(io::IO, published::PublishedToJavascript) = show(io, MIME("text/javascript"), published) -""" - publish_to_js(x) - -Make the object `x` available to the JS runtime of this cell. The returned string is a JS command that, when executed in this cell's output, gives the object. - -!!! warning - - This function is not yet public API, it will become public in the next weeks. Only use for experiments. - -# Example -```julia -let - x = Dict( - "data" => rand(Float64, 20), - "name" => "juliette", - ) - - HTML("\"" - - "\"") -end -``` -""" -publish_to_js(x) = publish_to_js(x, objectid2str(x)) +# TODO: This is the deprecated old function. Remove me at some point. +function publish_to_js(x) + @warn "Deprecated, use `AbstractPlutoDingetjes.Display.published_to_js(x)` instead of `PlutoRunner.publish_to_js(x)`." -function publish_to_js(x, id_start) assertpackable(x) - PublishedToJavascript( - published_object=x, - published_id_start=id_start, - cell_id=currently_running_cell_id[], - ) + PublishedToJavascript(x) end const Packable = Union{Nothing,Missing,String,Symbol,Int64,Int32,Int16,Int8,UInt64,UInt32,UInt16,UInt8,Float32,Float64,Bool,MIME,UUID,DateTime} -assertpackable(::Packable) = true +assertpackable(::Packable) = nothing assertpackable(t::Any) = throw(ArgumentError("Only simple objects can be shared with JS, like vectors and dictionaries. $(string(typeof(t))) is not compatible.")) -assertpackable(::Vector{<:Packable}) = true -assertpackable(::Dict{<:Packable,<:Packable}) = true +assertpackable(::Vector{<:Packable}) = nothing +assertpackable(::Dict{<:Packable,<:Packable}) = nothing assertpackable(x::Vector) = foreach(assertpackable, x) assertpackable(d::Dict) = let foreach(assertpackable, keys(d)) diff --git a/test/Dynamic.jl b/test/Dynamic.jl index f833933ff6..669017f640 100644 --- a/test/Dynamic.jl +++ b/test/Dynamic.jl @@ -199,20 +199,45 @@ end notebook = Notebook([ Cell("PlutoRunner.notebook_id[] |> Text"), + # These cells tests `core_published_to_js`, which is the function used by the official API (`AbtractPlutoDingetjes.Display.published_to_js`). Cell(cid, """ - let - # not actually public API but we test it anyways - a = PlutoRunner._publish(Dict( + begin + + a = Dict( "hello" => "world", "xx" => UInt8[6,7,8], - ), "aaa", Base.UUID("$cid")) - b = PlutoRunner._publish("cool", "bbb", Base.UUID("$cid")) - Text((a, b)) + ) + b = "cool" + + struct ZZZ + x + y + end + + function Base.show(io::IO, ::MIME"text/html", z::ZZZ) + write(io, "") + end + + ZZZ(a, b) + end + """), + Cell(""" + begin + struct ABC + x + end + ZZZ( + 123, + Dict("a" => 234, "b" => ABC(4)), + ) end """), - Cell("3"), + # This is the deprecated API: Cell("PlutoRunner.publish_to_js(Ref(4))"), - Cell("PlutoRunner.publish_to_js((ref=4,))"), + Cell("PlutoRunner.publish_to_js((ref=5,))"), Cell("x = Dict(:a => 6)"), Cell("PlutoRunner.publish_to_js(x)"), ]) @@ -220,11 +245,17 @@ end update_save_run!(🍭, notebook, notebook.cells) @test notebook.cells[1].output.body == notebook.notebook_id |> string - @test !notebook.cells[2].errored - a, b = Meta.parse(notebook.cells[2].output.body) |> eval + @test notebook.cells[2] |> noerror + @test notebook.cells[2].output.mime isa MIME"text/html" + + ab1, ab2 = keys(notebook.cells[2].published_objects) + @test occursin(ab1, notebook.cells[2].output.body) + @test occursin(ab2, notebook.cells[2].output.body) + + ab() = sort(collect(keys(notebook.cells[2].published_objects)); by=(s -> findfirst(s, notebook.cells[2].output.body) |> first)) + a, b = ab() + p = notebook.cells[2].published_objects - @test sort(collect(keys(p))) == sort([a,b]) - @test isempty(notebook.cells[3].published_objects) @test p[a] == Dict( "hello" => "world", @@ -236,18 +267,29 @@ end old_pb = p[b] update_save_run!(🍭, notebook, notebook.cells) p = notebook.cells[2].published_objects - a, b = Meta.parse(notebook.cells[2].output.body) |> eval + a, b = ab() @test p[a] == old_pa @test p[b] == old_pb @test !isempty(notebook.cells[2].published_objects) + # display should have failed + @test only(values(notebook.cells[3].published_objects)) == 123 + msg = notebook.cells[3].output.body[:msg] + @test occursin("Failed to show value", msg) + @test occursin("ABC is not compatible", msg) + + + setcode!(notebook.cells[2], "2") update_save_run!(🍭, notebook, notebook.cells) @test isempty(notebook.cells[2].published_objects) - + @test isempty(notebook.cells[2].published_objects) + + + @test notebook.cells[4].errored - @test !notebook.cells[5].errored + @test notebook.cells[5] |> noerror @test !isempty(notebook.cells[5].published_objects) diff --git a/test/frontend/__tests__/published_to_js.js b/test/frontend/__tests__/published_to_js.js new file mode 100644 index 0000000000..348b3cd6bf --- /dev/null +++ b/test/frontend/__tests__/published_to_js.js @@ -0,0 +1,58 @@ +import puppeteer from "puppeteer" +import { lastElement, saveScreenshot, getTestScreenshotPath, createPage } from "../helpers/common" +import { + getCellIds, + importNotebook, + waitForCellOutput, + getPlutoUrl, + prewarmPluto, + writeSingleLineInPlutoInput, + waitForNoUpdateOngoing, + shutdownCurrentNotebook, + setupPlutoBrowser, +} from "../helpers/pluto" + +describe("published_to_js", () => { + /** + * 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() + }) + beforeEach(async () => { + page = await createPage(browser) + await page.goto(getPlutoUrl(), { waitUntil: "networkidle0" }) + }) + afterEach(async () => { + await saveScreenshot(page) + await shutdownCurrentNotebook(page) + await page.close() + page = null + }) + afterAll(async () => { + await browser.close() + browser = null + }) + + it("Should correctly show published_to_js in cell output, and in logs", async () => { + await importNotebook(page, "published_to_js.jl") + await waitForNoUpdateOngoing(page, { polling: 100 }) + let output_of_published = await page.evaluate(() => { + return document.querySelector("#to_cell_output")?.textContent + }) + expect(output_of_published).toBe("[1,2,3] MAGIC!") + + // The log content is not shown, so #to_cell_log does not exist + let log_of_published = await page.evaluate(() => { + return document.querySelector("#to_cell_log")?.textContent + }) + // This test is currently broken, due to https://github.com/fonsp/Pluto.jl/issues/2092 + // expect(log_of_published).toBe("[4,5,6] MAGIC!") + }) +}) diff --git a/test/frontend/fixtures/published_to_js.jl b/test/frontend/fixtures/published_to_js.jl new file mode 100644 index 0000000000..1e8356e57c --- /dev/null +++ b/test/frontend/fixtures/published_to_js.jl @@ -0,0 +1,50 @@ +### A Pluto.jl notebook ### +# v0.19.27 + +using Markdown +using InteractiveUtils + +# ╔═╡ 2d69377e-23f8-11ee-116b-fb6a8f328528 +begin + using Pkg + Pkg.activate(temp=true) + # the latest versions of these packages: + Pkg.add(url="https://github.com/JuliaPluto/AbstractPlutoDingetjes.jl", rev="main") + Pkg.add("HypertextLiteral") +end + +# ╔═╡ 2ea26a4b-2d1e-4bcb-8b7b-cace79f7926a +begin + using AbstractPlutoDingetjes.Display: published_to_js + using HypertextLiteral +end + +# ╔═╡ 043829fc-af3a-40b9-bb4f-f848ab50eb25 +a = [1,2,3]; + +# ╔═╡ 2f4609fd-7361-4048-985a-2cc74bb25606 +@htl """ + +""" + +# ╔═╡ 28eba9fd-0416-49b8-966e-03a381c19ca7 +b = [4,5,6]; + +# ╔═╡ 0a4e8a19-6d43-4161-bb8c-1ebf8f8f68ba +@info @htl """ + +""" + +# ╔═╡ Cell order: +# ╠═2d69377e-23f8-11ee-116b-fb6a8f328528 +# ╠═2ea26a4b-2d1e-4bcb-8b7b-cace79f7926a +# ╠═043829fc-af3a-40b9-bb4f-f848ab50eb25 +# ╠═2f4609fd-7361-4048-985a-2cc74bb25606 +# ╠═28eba9fd-0416-49b8-966e-03a381c19ca7 +# ╠═0a4e8a19-6d43-4161-bb8c-1ebf8f8f68ba From f297b127214ea7790b344b5c7687a21044c6323f Mon Sep 17 00:00:00 2001 From: Paul Berg Date: Sun, 13 Aug 2023 11:52:29 +0200 Subject: [PATCH 27/52] Test Julia 1.10 (#2626) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Test Julia 1.10 * Disable Forward compat test 🤔 * Bump Symbolics to v5.5.1 in MacroAnalysis test --- .github/workflows/Test.yml | 2 +- test/MacroAnalysis.jl | 6 +-- test/packages/Basic.jl | 76 +++++++++++++++++++------------------- 3 files changed, 43 insertions(+), 41 deletions(-) diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index 37e73f3784..ecf12bbf1f 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -34,7 +34,7 @@ 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.7", "1.8", "1.9"] #, "nightly"] + julia-version: ["1.6", "1.7", "1.8", "1.9", "nightly"] os: [ubuntu-latest, macOS-latest, windows-latest] steps: diff --git a/test/MacroAnalysis.jl b/test/MacroAnalysis.jl index 171f9e8ce0..4c54571674 100644 --- a/test/MacroAnalysis.jl +++ b/test/MacroAnalysis.jl @@ -624,7 +624,7 @@ import Memoize: @memoize @testset "Package macro 2" begin 🍭.options.evaluation.workspace_use_distributed = true - + notebook = Notebook([ Cell("z = x^2 + y"), Cell("@variables x y"), @@ -632,7 +632,7 @@ import Memoize: @memoize begin import Pkg Pkg.activate(mktempdir()) - Pkg.add(Pkg.PackageSpec(name="Symbolics", version="1")) + Pkg.add(Pkg.PackageSpec(name="Symbolics", version="5.5.1")) import Symbolics: @variables end """), @@ -648,7 +648,7 @@ import Memoize: @memoize @test cell(1) |> noerror @test cell(2) |> noerror - @test cell(2) |> noerror + @test cell(3) |> noerror update_run!(🍭, notebook, notebook.cells) diff --git a/test/packages/Basic.jl b/test/packages/Basic.jl index 6a8af7e9f5..773be1ec71 100644 --- a/test/packages/Basic.jl +++ b/test/packages/Basic.jl @@ -446,50 +446,52 @@ import Distributed WorkspaceManager.unmake_workspace((🍭, notebook)) end - @testset "File format -- Forwards compat" begin - # Using Distributed, we will create a new Julia process in which we install Pluto 0.14.7 (before PlutoPkg). We run the new notebook file on the old Pluto. - p = Distributed.addprocs(1) |> first - - @test post_pkg_notebook isa String - - Distributed.remotecall_eval(Main, p, quote - path = tempname() - write(path, $(post_pkg_notebook)) - import Pkg - # optimization: - if isdefined(Pkg, :UPDATED_REGISTRY_THIS_SESSION) - Pkg.UPDATED_REGISTRY_THIS_SESSION[] = true - end + @static if VERSION ≤ v"1.9" + @testset "File format -- Forwards compat" begin + # Using Distributed, we will create a new Julia process in which we install Pluto 0.14.7 (before PlutoPkg). We run the new notebook file on the old Pluto. + p = Distributed.addprocs(1) |> first + + @test post_pkg_notebook isa String + + Distributed.remotecall_eval(Main, p, quote + path = tempname() + write(path, $(post_pkg_notebook)) + import Pkg + # optimization: + if isdefined(Pkg, :UPDATED_REGISTRY_THIS_SESSION) + Pkg.UPDATED_REGISTRY_THIS_SESSION[] = true + end - Pkg.activate(mktempdir()) - Pkg.add(Pkg.PackageSpec(;name="Pluto",version=v"0.14.7")) - import Pluto - @assert Pluto.PLUTO_VERSION == v"0.14.7" + Pkg.activate(mktempdir()) + Pkg.add(Pkg.PackageSpec(;name="Pluto",version=v"0.14.7")) + import Pluto + @assert Pluto.PLUTO_VERSION == v"0.14.7" - s = Pluto.ServerSession() - s.options.evaluation.workspace_use_distributed = false + s = Pluto.ServerSession() + s.options.evaluation.workspace_use_distributed = false - nb = Pluto.SessionActions.open(s, path; run_async=false) + nb = Pluto.SessionActions.open(s, path; run_async=false) - nothing - end) + nothing + end) - # Cells that use Example will error because the package is not installed. + # Cells that use Example will error because the package is not installed. - # @test Distributed.remotecall_eval(Main, p, quote - # nb.cells[1].errored == false - # end) - @test Distributed.remotecall_eval(Main, p, quote - nb.cells[2].errored == false - end) - # @test Distributed.remotecall_eval(Main, p, quote - # nb.cells[3].errored == false - # end) - # @test Distributed.remotecall_eval(Main, p, quote - # nb.cells[3].output.body == "25" - # end) + # @test Distributed.remotecall_eval(Main, p, quote + # nb.cells[1].errored == false + # end) + @test Distributed.remotecall_eval(Main, p, quote + nb.cells[2].errored == false + end) + # @test Distributed.remotecall_eval(Main, p, quote + # nb.cells[3].errored == false + # end) + # @test Distributed.remotecall_eval(Main, p, quote + # nb.cells[3].output.body == "25" + # end) - Distributed.rmprocs([p]) + Distributed.rmprocs([p]) + end end @testset "PkgUtils -- reset" begin From bfd7830925bd8932d1b83eee94218e8b66aed8fc Mon Sep 17 00:00:00 2001 From: Paul Berg Date: Thu, 24 Aug 2023 20:01:30 +0200 Subject: [PATCH 28/52] integrate JuliaSyntax.jl (#2526) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * integrate JuliaSyntax.jl * Delete test_bed.jl * improve display * update Parse.jl * Move code to PlutoRunner * Add tests * No stacktrace for syntax error in <=1.9 * Update TypeScriptCheck.yml * Update CodemirrorPlutoSetup.d.ts * Update React.jl * Update is_just_text.jl * Update TypeScriptCheck.yml * Fixes for the codemirror update * Update TypeScriptCheck.yml * Disable Forward compat test 🤔 * Set `z-index` for pluto-runarea to 19 (pluto-input.cm-editor.z-index - 1) * Change Docs test to adapt to nightly * tighten is_just_text --- frontend/components/Cell.js | 101 ++-- frontend/components/CellInput.js | 23 +- .../components/CellInput/highlight_line.js | 58 ++ .../CellInput/pluto_autocomplete.js | 6 +- frontend/components/CellOutput.js | 5 +- frontend/components/ErrorMessage.js | 59 +- frontend/editor.css | 14 +- frontend/imports/CodemirrorPlutoSetup.d.ts | 557 +++++++++++++++++- frontend/imports/CodemirrorPlutoSetup.js | 6 +- src/analysis/ExpressionExplorer.jl | 1 + src/analysis/Parse.jl | 11 +- src/analysis/is_just_text.jl | 18 +- src/runner/PlutoRunner.jl | 172 +++++- test/Dynamic.jl | 2 +- test/ExpressionExplorer.jl | 14 + test/React.jl | 24 + test/packages/Basic.jl | 20 +- 17 files changed, 1011 insertions(+), 80 deletions(-) diff --git a/frontend/components/Cell.js b/frontend/components/Cell.js index 247740152a..b43b843ae2 100644 --- a/frontend/components/Cell.js +++ b/frontend/components/Cell.js @@ -135,7 +135,31 @@ export const Cell = ({ const remount = useMemo(() => () => setKey(key + 1)) // cm_forced_focus is null, except when a line needs to be highlighted because it is part of a stack trace const [cm_forced_focus, set_cm_forced_focus] = useState(/** @type{any} */ (null)) + const [cm_highlighted_range, set_cm_highlighted_range] = useState(null) const [cm_highlighted_line, set_cm_highlighted_line] = useState(null) + const [cm_diagnostics, set_cm_diagnostics] = useState([]) + + useEffect(() => { + const diagnosticListener = (e) => { + if (e.detail.cell_id === cell_id) { + set_cm_diagnostics(e.detail.diagnostics) + } + } + window.addEventListener("cell_diagnostics", diagnosticListener) + return () => window.removeEventListener("cell_diagnostics", diagnosticListener) + }, [cell_id]) + + useEffect(() => { + const highlightRangeListener = (e) => { + if (e.detail.cell_id == cell_id && e.detail.from != null && e.detail.to != null) { + set_cm_highlighted_range({ from: e.detail.from, to: e.detail.to }) + } else { + set_cm_highlighted_range(null) + } + } + window.addEventListener("cell_highlight_range", highlightRangeListener) + return () => window.removeEventListener("cell_highlight_range", highlightRangeListener) + }, [cell_id]) useEffect(() => { const focusListener = (e) => { @@ -248,21 +272,21 @@ export const Cell = ({ key=${cell_key} ref=${node_ref} class=${cl({ - queued: queued || (waiting_to_run && is_process_ready), - running, - activate_animation, - errored, - selected, - code_differs: class_code_differs, - code_folded: class_code_folded, - skip_as_script, - running_disabled, - depends_on_disabled_cells, - depends_on_skipped_cells, - show_input, - shrunk: Object.values(logs).length > 0, - hooked_up: output?.has_pluto_hook_features ?? false, - })} + queued: queued || (waiting_to_run && is_process_ready), + running, + activate_animation, + errored, + selected, + code_differs: class_code_differs, + code_folded: class_code_folded, + skip_as_script, + running_disabled, + depends_on_disabled_cells, + depends_on_skipped_cells, + show_input, + shrunk: Object.values(logs).length > 0, + hooked_up: output?.has_pluto_hook_features ?? false, + })} id=${cell_id} > ${variables.map((name) => html``)} @@ -274,14 +298,14 @@ export const Cell = ({ - ${cell_api_ready ? html`<${CellOutput} errored=${errored} ...${output} cell_id=${cell_id} />` : html``} + ${cell_api_ready ? html`<${CellOutput} errored=${errored} ...${output} cell_id=${cell_id} />` : html``} <${CellInput} local_code=${cell_input_local?.code ?? code} remote_code=${code} @@ -308,7 +332,8 @@ export const Cell = ({ set_show_logs=${set_show_logs} set_cell_disabled=${set_cell_disabled} cm_highlighted_line=${cm_highlighted_line} - set_cm_highlighted_line=${set_cm_highlighted_line} + cm_highlighted_range=${cm_highlighted_range} + cm_diagnostics=${cm_diagnostics} onerror=${remount} /> ${show_logs && cell_api_ready @@ -320,8 +345,8 @@ export const Cell = ({ depends_on_disabled_cells=${depends_on_disabled_cells} on_run=${on_run} on_interrupt=${() => { - pluto_actions.interrupt_remote(cell_id) - }} + pluto_actions.interrupt_remote(cell_id) + }} set_cell_disabled=${set_cell_disabled} runtime=${runtime} running=${running} @@ -331,42 +356,42 @@ export const Cell = ({ /> ${skip_as_script - ? html`
{ - open_pluto_popup({ - type: "info", - source_element: e.target, - body: html`This cell is currently stored in the notebook file as a Julia comment, instead of code.
+ open_pluto_popup({ + type: "info", + source_element: e.target, + body: html`This cell is currently stored in the notebook file as a Julia comment, instead of code.
This way, it will not run when the notebook runs as a script outside of Pluto.
Use the context menu to enable it again`, - }) - }} + }) + }} >
` - : depends_on_skipped_cells + : depends_on_skipped_cells ? html`
{ - open_pluto_popup({ - type: "info", - source_element: e.target, - body: html`This cell is currently stored in the notebook file as a Julia comment, instead of code.
+ open_pluto_popup({ + type: "info", + source_element: e.target, + body: html`This cell is currently stored in the notebook file as a Julia comment, instead of code.
This way, it will not run when the notebook runs as a script outside of Pluto.
An upstream cell is indirectly disabling in file this one; enable the upstream one to affect this cell.`, - }) - }} + }) + }} >
` : null} @@ -388,7 +413,7 @@ export const IsolatedCell = ({ cell_input: { cell_id, metadata }, cell_result: { return html` ${cell_api_ready ? html`<${CellOutput} ...${output} cell_id=${cell_id} />` : html``} - ${show_logs ? html`<${Logs} logs=${Object.values(logs)} line_heights=${[15]} set_cm_highlighted_line=${() => {}} />` : null} + ${show_logs ? html`<${Logs} logs=${Object.values(logs)} line_heights=${[15]} set_cm_highlighted_line=${() => { }} />` : null} ` } diff --git a/frontend/components/CellInput.js b/frontend/components/CellInput.js index 9133b305a2..33cdb5e2e8 100644 --- a/frontend/components/CellInput.js +++ b/frontend/components/CellInput.js @@ -48,6 +48,7 @@ import { pythonLanguage, syntaxHighlighting, cssLanguage, + setDiagnostics, } from "../imports/CodemirrorPlutoSetup.js" import { markdown, html as htmlLang, javascript, sqlLang, python, julia_mixed } from "./CellInput/mixedParsers.js" @@ -59,7 +60,7 @@ import { cell_movement_plugin, prevent_holding_a_key_from_doing_things_across_ce import { pluto_paste_plugin } from "./CellInput/pluto_paste_plugin.js" import { bracketMatching } from "./CellInput/block_matcher_plugin.js" import { cl } from "../common/ClassTable.js" -import { HighlightLineFacet, highlightLinePlugin } from "./CellInput/highlight_line.js" +import { HighlightLineFacet, HighlightRangeFacet, highlightLinePlugin, highlightRangePlugin } from "./CellInput/highlight_line.js" import { commentKeymap } from "./CellInput/comment_mixed_parsers.js" import { ScopeStateField } from "./CellInput/scopestate_statefield.js" import { mod_d_command } from "./CellInput/mod_d_command.js" @@ -373,8 +374,10 @@ export const CellInput = ({ set_show_logs, set_cell_disabled, cm_highlighted_line, + cm_highlighted_range, metadata, global_definition_locations, + cm_diagnostics, }) => { let pluto_actions = useContext(PlutoActionsContext) const { disabled: running_disabled, skip_as_script } = metadata @@ -384,6 +387,7 @@ export const CellInput = ({ set_error(null) throw to_throw } + const notebook_id_ref = useRef(notebook_id) notebook_id_ref.current = notebook_id @@ -394,6 +398,7 @@ export const CellInput = ({ let nbpkg_compartment = useCompartment(newcm_ref, NotebookpackagesFacet.of(nbpkg)) let global_definitions_compartment = useCompartment(newcm_ref, GlobalDefinitionsFacet.of(global_definition_locations)) let highlighted_line_compartment = useCompartment(newcm_ref, HighlightLineFacet.of(cm_highlighted_line)) + let highlighted_range_compartment = useCompartment(newcm_ref, HighlightRangeFacet.of(cm_highlighted_range)) let editable_compartment = useCompartment(newcm_ref, EditorState.readOnly.of(disable_input)) let on_change_compartment = useCompartment( @@ -589,9 +594,11 @@ export const CellInput = ({ // Compartments coming from react state/props nbpkg_compartment, highlighted_line_compartment, + highlighted_range_compartment, global_definitions_compartment, editable_compartment, highlightLinePlugin(), + highlightRangePlugin(), // This is waaaay in front of the keys it is supposed to override, // Which is necessary because it needs to run before *any* keymap, @@ -713,6 +720,12 @@ export const CellInput = ({ // Wowww this has been enabled for some time now... wonder if there are issues about this yet ;) - DRAL awesome_line_wrapping, + // Reset diagnostics on change + EditorView.updateListener.of((update) => { + if (!update.docChanged) return + update.view.dispatch(setDiagnostics(update.state, [])) + }), + on_change_compartment, // This is my weird-ass extension that checks the AST and shows you where @@ -778,6 +791,14 @@ export const CellInput = ({ } }, []) + useEffect(() => { + if (newcm_ref.current == null) return + const cm = newcm_ref.current + const diagnostics = cm_diagnostics + + cm.dispatch(setDiagnostics(cm.state, diagnostics)) + }, [cm_diagnostics]) + // Effect to apply "remote_code" to the cell when it changes... // ideally this won't be necessary as we'll have actual multiplayer, // or something to tell the user that the cell is out of sync. diff --git a/frontend/components/CellInput/highlight_line.js b/frontend/components/CellInput/highlight_line.js index f66346f878..d5938350d4 100644 --- a/frontend/components/CellInput/highlight_line.js +++ b/frontend/components/CellInput/highlight_line.js @@ -4,6 +4,10 @@ const highlighted_line = Decoration.line({ attributes: { class: "cm-highlighted-line" }, }) +const highlighted_range = Decoration.mark({ + attributes: { class: "cm-highlighted-range" }, +}) + /** * @param {EditorView} view */ @@ -17,6 +21,22 @@ function create_line_decorations(view) { return Decoration.set([highlighted_line.range(line.from, line.from)]) } +/** + * @param {EditorView} view + */ +function create_range_decorations(view) { + let range = view.state.facet(HighlightRangeFacet) + if (range == null) { + return Decoration.set([]) + } + let { from, to } = range + if (from < 0 || from == to) { + return Decoration.set([]) + } + + return Decoration.set([highlighted_range.range(from, to)]) +} + /** * @type Facet */ @@ -25,6 +45,14 @@ export const HighlightLineFacet = Facet.define({ compare: (a, b) => a === b, }) +/** + * @type Facet<{from: number, to: number}?, {from: number, to: number}?> + */ +export const HighlightRangeFacet = Facet.define({ + combine: (values) => values[0], + compare: (a, b) => a === b, +}) + export const highlightLinePlugin = () => ViewPlugin.fromClass( class { @@ -53,3 +81,33 @@ export const highlightLinePlugin = () => decorations: (v) => v.decorations, } ) + + +export const highlightRangePlugin = () => + ViewPlugin.fromClass( + class { + updateDecos(view) { + this.decorations = create_range_decorations(view) + } + + /** + * @param {EditorView} view + */ + constructor(view) { + this.decorations = Decoration.set([]) + this.updateDecos(view) + } + + /** + * @param {ViewUpdate} update + */ + update(update) { + if (update.docChanged || update.state.facet(HighlightRangeFacet) !== update.startState.facet(HighlightRangeFacet)) { + this.updateDecos(update.view) + } + } + }, + { + decorations: (v) => v.decorations, + } + ) diff --git a/frontend/components/CellInput/pluto_autocomplete.js b/frontend/components/CellInput/pluto_autocomplete.js index 2130d6a484..94885157fc 100644 --- a/frontend/components/CellInput/pluto_autocomplete.js +++ b/frontend/components/CellInput/pluto_autocomplete.js @@ -131,7 +131,11 @@ let update_docs_from_autocomplete_selection = (on_update_doc_query) => { // The nice thing about this is that we can use the resulting state from the transaction, // without updating the actual state of the editor. let result_transaction = update.state.update({ - changes: { from: selected_option.source.from, to: selected_option.source.to, insert: text_to_apply }, + changes: { + from: selected_option.source.from, + to: Math.min(selected_option.source.to, update.state.doc.length), + insert: text_to_apply, + }, }) // So we can use `get_selected_doc_from_state` on our virtual state diff --git a/frontend/components/CellOutput.js b/frontend/components/CellOutput.js index ff4188cf36..8eb7b4943a 100644 --- a/frontend/components/CellOutput.js +++ b/frontend/components/CellOutput.js @@ -1,6 +1,6 @@ import { html, Component, useRef, useLayoutEffect, useContext } from "../imports/Preact.js" -import { ErrorMessage } from "./ErrorMessage.js" +import { ErrorMessage, ParseError } from "./ErrorMessage.js" import { TreeView, TableView, DivElement } from "./TreeView.js" import { @@ -148,6 +148,9 @@ export const OutputBody = ({ mime, body, cell_id, persist_js_state = false, last case "application/vnd.pluto.table+object": return html`<${TableView} cell_id=${cell_id} body=${body} persist_js_state=${persist_js_state} />` break + case "application/vnd.pluto.parseerror+object": + return html`
<${ParseError} cell_id=${cell_id} ...${body} />
` + break case "application/vnd.pluto.stacktrace+object": return html`
<${ErrorMessage} cell_id=${cell_id} ...${body} />
` break diff --git a/frontend/components/ErrorMessage.js b/frontend/components/ErrorMessage.js index 173243ee9b..60e83a1515 100644 --- a/frontend/components/ErrorMessage.js +++ b/frontend/components/ErrorMessage.js @@ -1,5 +1,8 @@ import { PlutoActionsContext } from "../common/PlutoContext.js" -import { html, useContext, useState } from "../imports/Preact.js" +import { EditorState, EditorView, julia_andrey, lineNumbers, syntaxHighlighting } from "../imports/CodemirrorPlutoSetup.js" +import { html, useContext, useEffect, useLayoutEffect, useRef, useState } from "../imports/Preact.js" +import { pluto_syntax_colors } from "./CellInput.js" +import { Editor } from "./Editor.js" const StackFrameFilename = ({ frame, cell_id }) => { const sep_index = frame.file.indexOf("#==#") @@ -38,6 +41,43 @@ const Funccall = ({ frame }) => { const insert_commas_and_and = (/** @type {any[]} */ xs) => xs.flatMap((x, i) => (i === xs.length - 1 ? [x] : i === xs.length - 2 ? [x, " and "] : [x, ", "])) +export const ParseError = ({ cell_id, diagnostics }) => { + useEffect(() => { + window.dispatchEvent( + new CustomEvent("cell_diagnostics", { + detail: { + cell_id, + diagnostics, + }, + }) + ) + return () => window.dispatchEvent(new CustomEvent("cell_diagnostics", { detail: { cell_id, diagnostics: [] } })) + }, [diagnostics]) + + return html` + +

Syntax error

+
+
    + ${diagnostics.map( + ({ message, from, to, line }) => + html`
  1. // NOTE: this could be moved move to `StackFrameFilename` + window.dispatchEvent(new CustomEvent("cell_highlight_range", { detail: { cell_id, from, to }})) + } + onmouseleave=${() => + window.dispatchEvent(new CustomEvent("cell_highlight_range", { detail: { cell_id, from: null, to: null }})) + } + > + ${message}@ + <${StackFrameFilename} frame=${{file: "#==#" + cell_id, line}} cell_id=${cell_id} /> +
  2. `) + } +
+
+
+ `; +} + export const ErrorMessage = ({ msg, stacktrace, cell_id }) => { let pluto_actions = useContext(PlutoActionsContext) const default_rewriter = { @@ -62,9 +102,9 @@ export const ErrorMessage = ({ msg, stacktrace, cell_id }) => { { - e.preventDefault() - pluto_actions.split_remote_cell(cell_id, boundaries, true) - }} + e.preventDefault() + pluto_actions.split_remote_cell(cell_id, boundaries, true) + }} >Split this cell into ${boundaries.length} cells, or

` @@ -137,6 +177,11 @@ export const ErrorMessage = ({ msg, stacktrace, cell_id }) => { } }), }, + { + pattern: /^syntax: (.*)$/, + display: default_rewriter.display, + show_stacktrace: () => false, + }, { pattern: /^UndefVarError: (.*) not defined\.?$/, display: (/** @type{string} */ x) => { @@ -183,14 +228,14 @@ export const ErrorMessage = ({ msg, stacktrace, cell_id }) => { : html`
    ${stacktrace.map( - (frame) => - html`
  1. + (frame) => + html`
  2. <${Funccall} frame=${frame} /> @ <${StackFrameFilename} frame=${frame} cell_id=${cell_id} /> ${frame.inlined ? html`[inlined]` : null}
  3. ` - )} + )}
`} ` diff --git a/frontend/editor.css b/frontend/editor.css index c573b7c38c..73964d87d5 100644 --- a/frontend/editor.css +++ b/frontend/editor.css @@ -1232,6 +1232,7 @@ pluto-input .cm-editor .cm-line { transition: background-color 0.15s ease-in-out; } +pluto-input .cm-editor span.cm-highlighted-range, pluto-input .cm-editor .cm-line.cm-highlighted-line { background-color: #bdbdbd68; border-radius: 3px; @@ -1934,6 +1935,9 @@ pluto-runarea { border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; border-top: none; + /* One less than z-index for pluto-input .cm-editor. + Otherwise it gets on top of the tooltips */ + z-index: 19; } pluto-runarea > span { @@ -3048,6 +3052,12 @@ button.floating_back_button { border-radius: 4px; } +.cm-tooltip-lint { + font-family: "JuliaMono"; + font-size: 0.75rem; + z-index: 100; +} + .cm-tooltip-autocomplete { max-height: calc(20 * 16px); box-sizing: content-box; @@ -3185,10 +3195,10 @@ pluto-input .cm-editor .cm-content { padding: 2px 0px; } -.cm-editor .cm-selectionBackground { +.cm-editor .cm-scroller > .cm-selectionLayer .cm-selectionBackground { background: var(--cm-selection-background-blurred); } -.cm-editor.cm-focused .cm-selectionBackground { +.cm-editor.cm-focused .cm-scroller > .cm-selectionLayer .cm-selectionBackground { background: var(--cm-selection-background); } diff --git a/frontend/imports/CodemirrorPlutoSetup.d.ts b/frontend/imports/CodemirrorPlutoSetup.d.ts index 6919393ed4..4e298e6d9d 100644 --- a/frontend/imports/CodemirrorPlutoSetup.d.ts +++ b/frontend/imports/CodemirrorPlutoSetup.d.ts @@ -1865,6 +1865,17 @@ declare type Attrs = { [name: string]: string; }; +/** +Basic rectangle type. +*/ +interface Rect { + readonly left: number; + readonly right: number; + readonly top: number; + readonly bottom: number; +} +declare type ScrollStrategy = "nearest" | "start" | "end" | "center"; + interface MarkDecorationSpec { /** Whether the mark covers its start and end position or not. This @@ -2013,7 +2024,7 @@ declare abstract class WidgetType { couldn't (in which case the widget will be redrawn). The default implementation just returns false. */ - updateDOM(dom: HTMLElement): boolean; + updateDOM(dom: HTMLElement, view: EditorView): boolean; /** The estimated height this widget will have, to be used when estimating the height of content that hasn't been drawn. May @@ -2028,6 +2039,14 @@ declare abstract class WidgetType { */ ignoreEvent(event: Event): boolean; /** + Override the way screen coordinates for positions at/in the + widget are found. `pos` will be the offset into the widget, and + `side` the side of the position that is being queried—less than + zero for before, greater than zero for after, and zero for + directly at that position. + */ + coordsAt(dom: HTMLElement, pos: number, side: number): Rect | null; + /** This is called when the an instance of the widget is removed from the editor view. */ @@ -2130,17 +2149,6 @@ declare abstract class Decoration extends RangeValue { static none: DecorationSet; } -/** -Basic rectangle type. -*/ -interface Rect { - readonly left: number; - readonly right: number; - readonly top: number; - readonly bottom: number; -} -declare type ScrollStrategy = "nearest" | "start" | "end" | "center"; - /** Command functions are used in key bindings and other types of user actions. Given an editor view, they check whether their effect can @@ -2841,6 +2849,11 @@ declare class EditorView { */ static inputHandler: Facet<(view: EditorView, from: number, to: number, text: string) => boolean, readonly ((view: EditorView, from: number, to: number, text: string) => boolean)[]>; /** + This facet can be used to provide functions that create effects + to be dispatched when the editor's focus state changes. + */ + static focusChangeEffect: Facet<(state: EditorState, focusing: boolean) => StateEffect | null, readonly ((state: EditorState, focusing: boolean) => StateEffect | null)[]>; + /** By default, the editor assumes all its content has the same [text direction](https://codemirror.net/6/docs/ref/#view.Direction). Configure this with a `true` value to make it read the text direction of every (rendered) @@ -3216,99 +3229,458 @@ Create a line number gutter extension. */ declare function lineNumbers(config?: LineNumberConfig): Extension; +/** +Highlighting tags are markers that denote a highlighting category. +They are [associated](#highlight.styleTags) with parts of a syntax +tree by a language mode, and then mapped to an actual CSS style by +a [highlighter](#highlight.Highlighter). + +Because syntax tree node types and highlight styles have to be +able to talk the same language, CodeMirror uses a mostly _closed_ +[vocabulary](#highlight.tags) of syntax tags (as opposed to +traditional open string-based systems, which make it hard for +highlighting themes to cover all the tokens produced by the +various languages). + +It _is_ possible to [define](#highlight.Tag^define) your own +highlighting tags for system-internal use (where you control both +the language package and the highlighter), but such tags will not +be picked up by regular highlighters (though you can derive them +from standard tags to allow highlighters to fall back to those). +*/ declare class Tag { + /** + The set of this tag and all its parent tags, starting with + this one itself and sorted in order of decreasing specificity. + */ readonly set: Tag[]; + /** + Define a new tag. If `parent` is given, the tag is treated as a + sub-tag of that parent, and + [highlighters](#highlight.tagHighlighter) that don't mention + this tag will try to fall back to the parent tag (or grandparent + tag, etc). + */ static define(parent?: Tag): Tag; + /** + Define a tag _modifier_, which is a function that, given a tag, + will return a tag that is a subtag of the original. Applying the + same modifier to a twice tag will return the same value (`m1(t1) + == m1(t1)`) and applying multiple modifiers will, regardless or + order, produce the same tag (`m1(m2(t1)) == m2(m1(t1))`). + + When multiple modifiers are applied to a given base tag, each + smaller set of modifiers is registered as a parent, so that for + example `m1(m2(m3(t1)))` is a subtype of `m1(m2(t1))`, + `m1(m3(t1)`, and so on. + */ static defineModifier(): (tag: Tag) => Tag; } +/** +A highlighter defines a mapping from highlighting tags and +language scopes to CSS class names. They are usually defined via +[`tagHighlighter`](#highlight.tagHighlighter) or some wrapper +around that, but it is also possible to implement them from +scratch. +*/ interface Highlighter { + /** + Get the set of classes that should be applied to the given set + of highlighting tags, or null if this highlighter doesn't assign + a style to the tags. + */ style(tags: readonly Tag[]): string | null; + /** + When given, the highlighter will only be applied to trees on + whose [top](#common.NodeType.isTop) node this predicate returns + true. + */ scope?(node: NodeType): boolean; } +/** +The default set of highlighting [tags](#highlight.Tag). + +This collection is heavily biased towards programming languages, +and necessarily incomplete. A full ontology of syntactic +constructs would fill a stack of books, and be impractical to +write themes for. So try to make do with this set. If all else +fails, [open an +issue](https://github.com/codemirror/codemirror.next) to propose a +new tag, or [define](#highlight.Tag^define) a local custom tag for +your use case. + +Note that it is not obligatory to always attach the most specific +tag possible to an element—if your grammar can't easily +distinguish a certain type of element (such as a local variable), +it is okay to style it as its more general variant (a variable). + +For tags that extend some parent tag, the documentation links to +the parent. +*/ declare const tags: { + /** + A comment. + */ comment: Tag; + /** + A line [comment](#highlight.tags.comment). + */ lineComment: Tag; + /** + A block [comment](#highlight.tags.comment). + */ blockComment: Tag; + /** + A documentation [comment](#highlight.tags.comment). + */ docComment: Tag; + /** + Any kind of identifier. + */ name: Tag; + /** + The [name](#highlight.tags.name) of a variable. + */ variableName: Tag; + /** + A type [name](#highlight.tags.name). + */ typeName: Tag; + /** + A tag name (subtag of [`typeName`](#highlight.tags.typeName)). + */ tagName: Tag; + /** + A property or field [name](#highlight.tags.name). + */ propertyName: Tag; + /** + An attribute name (subtag of [`propertyName`](#highlight.tags.propertyName)). + */ attributeName: Tag; + /** + The [name](#highlight.tags.name) of a class. + */ className: Tag; + /** + A label [name](#highlight.tags.name). + */ labelName: Tag; + /** + A namespace [name](#highlight.tags.name). + */ namespace: Tag; + /** + The [name](#highlight.tags.name) of a macro. + */ macroName: Tag; + /** + A literal value. + */ literal: Tag; + /** + A string [literal](#highlight.tags.literal). + */ string: Tag; + /** + A documentation [string](#highlight.tags.string). + */ docString: Tag; + /** + A character literal (subtag of [string](#highlight.tags.string)). + */ character: Tag; + /** + An attribute value (subtag of [string](#highlight.tags.string)). + */ attributeValue: Tag; + /** + A number [literal](#highlight.tags.literal). + */ number: Tag; + /** + An integer [number](#highlight.tags.number) literal. + */ integer: Tag; + /** + A floating-point [number](#highlight.tags.number) literal. + */ float: Tag; + /** + A boolean [literal](#highlight.tags.literal). + */ bool: Tag; + /** + Regular expression [literal](#highlight.tags.literal). + */ regexp: Tag; + /** + An escape [literal](#highlight.tags.literal), for example a + backslash escape in a string. + */ escape: Tag; + /** + A color [literal](#highlight.tags.literal). + */ color: Tag; + /** + A URL [literal](#highlight.tags.literal). + */ url: Tag; + /** + A language keyword. + */ keyword: Tag; + /** + The [keyword](#highlight.tags.keyword) for the self or this + object. + */ self: Tag; + /** + The [keyword](#highlight.tags.keyword) for null. + */ null: Tag; + /** + A [keyword](#highlight.tags.keyword) denoting some atomic value. + */ atom: Tag; + /** + A [keyword](#highlight.tags.keyword) that represents a unit. + */ unit: Tag; + /** + A modifier [keyword](#highlight.tags.keyword). + */ modifier: Tag; + /** + A [keyword](#highlight.tags.keyword) that acts as an operator. + */ operatorKeyword: Tag; + /** + A control-flow related [keyword](#highlight.tags.keyword). + */ controlKeyword: Tag; + /** + A [keyword](#highlight.tags.keyword) that defines something. + */ definitionKeyword: Tag; + /** + A [keyword](#highlight.tags.keyword) related to defining or + interfacing with modules. + */ moduleKeyword: Tag; + /** + An operator. + */ operator: Tag; + /** + An [operator](#highlight.tags.operator) that dereferences something. + */ derefOperator: Tag; + /** + Arithmetic-related [operator](#highlight.tags.operator). + */ arithmeticOperator: Tag; + /** + Logical [operator](#highlight.tags.operator). + */ logicOperator: Tag; + /** + Bit [operator](#highlight.tags.operator). + */ bitwiseOperator: Tag; + /** + Comparison [operator](#highlight.tags.operator). + */ compareOperator: Tag; + /** + [Operator](#highlight.tags.operator) that updates its operand. + */ updateOperator: Tag; + /** + [Operator](#highlight.tags.operator) that defines something. + */ definitionOperator: Tag; + /** + Type-related [operator](#highlight.tags.operator). + */ typeOperator: Tag; + /** + Control-flow [operator](#highlight.tags.operator). + */ controlOperator: Tag; + /** + Program or markup punctuation. + */ punctuation: Tag; + /** + [Punctuation](#highlight.tags.punctuation) that separates + things. + */ separator: Tag; + /** + Bracket-style [punctuation](#highlight.tags.punctuation). + */ bracket: Tag; + /** + Angle [brackets](#highlight.tags.bracket) (usually `<` and `>` + tokens). + */ angleBracket: Tag; + /** + Square [brackets](#highlight.tags.bracket) (usually `[` and `]` + tokens). + */ squareBracket: Tag; + /** + Parentheses (usually `(` and `)` tokens). Subtag of + [bracket](#highlight.tags.bracket). + */ paren: Tag; + /** + Braces (usually `{` and `}` tokens). Subtag of + [bracket](#highlight.tags.bracket). + */ brace: Tag; + /** + Content, for example plain text in XML or markup documents. + */ content: Tag; + /** + [Content](#highlight.tags.content) that represents a heading. + */ heading: Tag; + /** + A level 1 [heading](#highlight.tags.heading). + */ heading1: Tag; + /** + A level 2 [heading](#highlight.tags.heading). + */ heading2: Tag; + /** + A level 3 [heading](#highlight.tags.heading). + */ heading3: Tag; + /** + A level 4 [heading](#highlight.tags.heading). + */ heading4: Tag; + /** + A level 5 [heading](#highlight.tags.heading). + */ heading5: Tag; + /** + A level 6 [heading](#highlight.tags.heading). + */ heading6: Tag; + /** + A prose separator (such as a horizontal rule). + */ contentSeparator: Tag; + /** + [Content](#highlight.tags.content) that represents a list. + */ list: Tag; + /** + [Content](#highlight.tags.content) that represents a quote. + */ quote: Tag; + /** + [Content](#highlight.tags.content) that is emphasized. + */ emphasis: Tag; + /** + [Content](#highlight.tags.content) that is styled strong. + */ strong: Tag; + /** + [Content](#highlight.tags.content) that is part of a link. + */ link: Tag; + /** + [Content](#highlight.tags.content) that is styled as code or + monospace. + */ monospace: Tag; + /** + [Content](#highlight.tags.content) that has a strike-through + style. + */ strikethrough: Tag; + /** + Inserted text in a change-tracking format. + */ inserted: Tag; + /** + Deleted text. + */ deleted: Tag; + /** + Changed text. + */ changed: Tag; + /** + An invalid or unsyntactic element. + */ invalid: Tag; + /** + Metadata or meta-instruction. + */ meta: Tag; + /** + [Metadata](#highlight.tags.meta) that applies to the entire + document. + */ documentMeta: Tag; + /** + [Metadata](#highlight.tags.meta) that annotates or adds + attributes to a given syntactic element. + */ annotation: Tag; + /** + Processing instruction or preprocessor directive. Subtag of + [meta](#highlight.tags.meta). + */ processingInstruction: Tag; + /** + [Modifier](#highlight.Tag^defineModifier) that indicates that a + given element is being defined. Expected to be used with the + various [name](#highlight.tags.name) tags. + */ definition: (tag: Tag) => Tag; + /** + [Modifier](#highlight.Tag^defineModifier) that indicates that + something is constant. Mostly expected to be used with + [variable names](#highlight.tags.variableName). + */ constant: (tag: Tag) => Tag; + /** + [Modifier](#highlight.Tag^defineModifier) used to indicate that + a [variable](#highlight.tags.variableName) or [property + name](#highlight.tags.propertyName) is being called or defined + as a function. + */ function: (tag: Tag) => Tag; + /** + [Modifier](#highlight.Tag^defineModifier) that can be applied to + [names](#highlight.tags.name) to indicate that they belong to + the language's standard environment. + */ standard: (tag: Tag) => Tag; + /** + [Modifier](#highlight.Tag^defineModifier) that indicates a given + [names](#highlight.tags.name) is local to some scope. + */ local: (tag: Tag) => Tag; + /** + A generic variant [modifier](#highlight.Tag^defineModifier) that + can be used to tag language-specific alternative variants of + some common tag. It is recommended for themes to define special + forms of at least the [string](#highlight.tags.string) and + [variable name](#highlight.tags.variableName) tags, since those + come up a lot. + */ special: (tag: Tag) => Tag; }; @@ -3561,9 +3933,9 @@ declare class LanguageDescription { static matchLanguageName(descs: readonly LanguageDescription[], name: string, fuzzy?: boolean): LanguageDescription | null; } /** -Facet for overriding the unit by which indentation happens. -Should be a string consisting either entirely of spaces or -entirely of tabs. When not set, this defaults to 2 spaces. +Facet for overriding the unit by which indentation happens. Should +be a string consisting either entirely of the same whitespace +character. When not set, this defaults to 2 spaces. */ declare const indentUnit: Facet; /** @@ -3844,7 +4216,7 @@ The default keymap. Includes all bindings from - Shift-Alt-ArrowUp: [`copyLineUp`](https://codemirror.net/6/docs/ref/#commands.copyLineUp) - Shift-Alt-ArrowDown: [`copyLineDown`](https://codemirror.net/6/docs/ref/#commands.copyLineDown) - Escape: [`simplifySelection`](https://codemirror.net/6/docs/ref/#commands.simplifySelection) -- Ctrl-Enter (Comd-Enter on macOS): [`insertBlankLine`](https://codemirror.net/6/docs/ref/#commands.insertBlankLine) +- Ctrl-Enter (Cmd-Enter on macOS): [`insertBlankLine`](https://codemirror.net/6/docs/ref/#commands.insertBlankLine) - Alt-l (Ctrl-l on macOS): [`selectLine`](https://codemirror.net/6/docs/ref/#commands.selectLine) - Ctrl-i (Cmd-i on macOS): [`selectParentSyntax`](https://codemirror.net/6/docs/ref/#commands.selectParentSyntax) - Ctrl-[ (Cmd-[ on macOS): [`indentLess`](https://codemirror.net/6/docs/ref/#commands.indentLess) @@ -3933,6 +4305,19 @@ interface CompletionConfig { position: number; }[]; /** + By default, [info](https://codemirror.net/6/docs/ref/#autocomplet.Completion.info) tooltips are + placed to the side of the selected. This option can be used to + override that. It will be given rectangles for the list of + completions, the selected option, the info element, and the + availble [tooltip space](https://codemirror.net/6/docs/ref/#view.tooltips^config.tooltipSpace), + and should return style and/or class strings for the info + element. + */ + positionInfo?: (view: EditorView, list: Rect, option: Rect, info: Rect, space: Rect) => { + style?: string; + class?: string; + }; + /** The comparison function to use when sorting completions with the same match score. Defaults to using [`localeCompare`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare). @@ -3997,6 +4382,39 @@ interface Completion { down the list, a positive number moves it up. */ boost?: number; + /** + Can be used to divide the completion list into sections. + Completions in a given section (matched by name) will be grouped + together, with a heading above them. Options without section + will appear above all sections. A string value is equivalent to + a `{name}` object. + */ + section?: string | CompletionSection; +} +/** +Object used to describe a completion +[section](https://codemirror.net/6/docs/ref/#autocomplete.Completion.section). It is recommended to +create a shared object used by all the completions in a given +section. +*/ +interface CompletionSection { + /** + The name of the section. If no `render` method is present, this + will be displayed above the options. + */ + name: string; + /** + An optional function that renders the section header. Since the + headers are shown inside a list, you should make sure the + resulting element has a `display: list-item` style. + */ + header?: (section: CompletionSection) => HTMLElement; + /** + By default, sections are ordered alphabetically by name. To + specify an explicit order, `rank` can be used. Sections with a + lower rank will be shown above sections with a higher rank. + */ + rank?: number; } /** An instance of this is passed to completion source functions. @@ -4192,7 +4610,7 @@ interpreted as indicating a placeholder. declare function snippet(template: string): (editor: { state: EditorState; dispatch: (tr: Transaction) => void; -}, _completion: Completion, from: number, to: number) => void; +}, completion: Completion, from: number, to: number) => void; /** A command that clears the active snippet, if any. */ @@ -4347,6 +4765,7 @@ type index_d_Completion = Completion; type index_d_CompletionContext = CompletionContext; declare const index_d_CompletionContext: typeof CompletionContext; type index_d_CompletionResult = CompletionResult; +type index_d_CompletionSection = CompletionSection; type index_d_CompletionSource = CompletionSource; declare const index_d_acceptCompletion: typeof acceptCompletion; declare const index_d_autocompletion: typeof autocompletion; @@ -4381,6 +4800,7 @@ declare namespace index_d { index_d_Completion as Completion, index_d_CompletionContext as CompletionContext, index_d_CompletionResult as CompletionResult, + index_d_CompletionSection as CompletionSection, index_d_CompletionSource as CompletionSource, index_d_acceptCompletion as acceptCompletion, index_d_autocompletion as autocompletion, @@ -4697,6 +5117,101 @@ declare function html(config?: { nestedAttributes?: NestedAttr[]; }): LanguageSupport; +/** +Describes a problem or hint for a piece of code. +*/ +interface Diagnostic { + /** + The start position of the relevant text. + */ + from: number; + /** + The end position. May be equal to `from`, though actually + covering text is preferable. + */ + to: number; + /** + The severity of the problem. This will influence how it is + displayed. + */ + severity: "info" | "warning" | "error"; + /** + An optional source string indicating where the diagnostic is + coming from. You can put the name of your linter here, if + applicable. + */ + source?: string; + /** + The message associated with this diagnostic. + */ + message: string; + /** + An optional custom rendering function that displays the message + as a DOM node. + */ + renderMessage?: () => Node; + /** + An optional array of actions that can be taken on this + diagnostic. + */ + actions?: readonly Action[]; +} +/** +An action associated with a diagnostic. +*/ +interface Action { + /** + The label to show to the user. Should be relatively short. + */ + name: string; + /** + The function to call when the user activates this action. Is + given the diagnostic's _current_ position, which may have + changed since the creation of the diagnostic, due to editing. + */ + apply: (view: EditorView, from: number, to: number) => void; +} +declare type DiagnosticFilter = (diagnostics: readonly Diagnostic[]) => Diagnostic[]; +interface LintConfig { + /** + Time to wait (in milliseconds) after a change before running + the linter. Defaults to 750ms. + */ + delay?: number; + /** + Optional predicate that can be used to indicate when diagnostics + need to be recomputed. Linting is always re-done on document + changes. + */ + needsRefresh?: null | ((update: ViewUpdate) => boolean); + /** + Optional filter to determine which diagnostics produce markers + in the content. + */ + markerFilter?: null | DiagnosticFilter; + /** + Filter applied to a set of diagnostics shown in a tooltip. No + tooltip will appear if the empty set is returned. + */ + tooltipFilter?: null | DiagnosticFilter; +} +/** +Returns a transaction spec which updates the current set of +diagnostics, and enables the lint extension if if wasn't already +active. +*/ +declare function setDiagnostics(state: EditorState, diagnostics: readonly Diagnostic[]): TransactionSpec; +/** +The type of a function that produces diagnostics. +*/ +declare type LintSource = (view: EditorView) => readonly Diagnostic[] | Promise; +/** +Given a diagnostic source, this function returns an extension that +enables linting with that source. It will be called whenever the +editor is idle (after its content changed). +*/ +declare function linter(source: LintSource, config?: LintConfig): Extension; + /** A language provider based on the [Lezer JavaScript parser](https://github.com/lezer-parser/javascript), extended with @@ -4832,6 +5347,12 @@ interface SQLConfig { */ tables?: readonly Completion[]; /** + Similar to `tables`, if you want to provide completion objects + for your schemas rather than using the generated ones, pass them + here. + */ + schemas?: readonly Completion[]; + /** When given, columns from the named table can be completed directly at the top level. */ @@ -4893,4 +5414,4 @@ Create an instance of the collaborative editing plugin. */ declare function collab(config?: CollabConfig): Extension; -export { Annotation, Compartment, Decoration, EditorSelection, EditorState, EditorView, Facet, HighlightStyle, NodeProp, PostgreSQL, SelectionRange, StateEffect, StateField, Text, Transaction, TreeCursor, ViewPlugin, ViewUpdate, WidgetType, index_d as autocomplete, bracketMatching, closeBrackets, closeBracketsKeymap, collab, combineConfig, completionKeymap, css, cssLanguage, defaultHighlightStyle, defaultKeymap, drawSelection, foldGutter, foldKeymap, highlightSelectionMatches, highlightSpecialChars, history, historyKeymap, html, htmlLanguage, indentLess, indentMore, indentOnInput, indentUnit, javascript, javascriptLanguage, julia as julia_andrey, keymap, lineNumbers, markdown, markdownLanguage, parseCode, parseMixed, placeholder, python, pythonLanguage, rectangularSelection, searchKeymap, selectNextOccurrence, sql, syntaxHighlighting, syntaxTree, syntaxTreeAvailable, tags }; +export { Annotation, Compartment, Decoration, Diagnostic, EditorSelection, EditorState, EditorView, Facet, HighlightStyle, NodeProp, PostgreSQL, SelectionRange, StateEffect, StateField, Text, Transaction, TreeCursor, ViewPlugin, ViewUpdate, WidgetType, index_d as autocomplete, bracketMatching, closeBrackets, closeBracketsKeymap, collab, combineConfig, completionKeymap, css, cssLanguage, defaultHighlightStyle, defaultKeymap, drawSelection, foldGutter, foldKeymap, highlightSelectionMatches, highlightSpecialChars, history, historyKeymap, html, htmlLanguage, indentLess, indentMore, indentOnInput, indentUnit, javascript, javascriptLanguage, julia as julia_andrey, keymap, lineNumbers, linter, markdown, markdownLanguage, parseCode, parseMixed, placeholder, python, pythonLanguage, rectangularSelection, searchKeymap, selectNextOccurrence, setDiagnostics, sql, syntaxHighlighting, syntaxTree, syntaxTreeAvailable, tags }; diff --git a/frontend/imports/CodemirrorPlutoSetup.js b/frontend/imports/CodemirrorPlutoSetup.js index ce8c635152..da54f20909 100644 --- a/frontend/imports/CodemirrorPlutoSetup.js +++ b/frontend/imports/CodemirrorPlutoSetup.js @@ -59,10 +59,14 @@ import { css, cssLanguage, selectNextOccurrence, + linter, + setDiagnostics, //@ts-ignore -} from "https://cdn.jsdelivr.net/gh/JuliaPluto/codemirror-pluto-setup@0.28.4/dist/index.es.min.js" +} from "https://cdn.jsdelivr.net/gh/JuliaPluto/codemirror-pluto-setup@1234.0.0/dist/index.es.min.js" export { + linter, + setDiagnostics, EditorState, EditorSelection, Compartment, diff --git a/src/analysis/ExpressionExplorer.jl b/src/analysis/ExpressionExplorer.jl index 1fd4b5837c..67edc5c82a 100644 --- a/src/analysis/ExpressionExplorer.jl +++ b/src/analysis/ExpressionExplorer.jl @@ -1348,6 +1348,7 @@ function can_be_function_wrapped(x::Expr) x.head === :using || x.head === :import || x.head === :module || + x.head === :incomplete || # Only bail on named functions, but anonymous functions (args[1].head == :tuple) are fine. # TODO Named functions INSIDE other functions should be fine too (x.head === :function && !Meta.isexpr(x.args[1], :tuple)) || diff --git a/src/analysis/Parse.jl b/src/analysis/Parse.jl index 200b5faf4b..b3ac5162d5 100644 --- a/src/analysis/Parse.jl +++ b/src/analysis/Parse.jl @@ -1,4 +1,5 @@ import .ExpressionExplorer +import Markdown "Generate a file name to be given to the parser (will show up in stack traces)." pluto_filename(notebook::Notebook, cell::Cell)::String = notebook.path * "#==#" * string(cell.cell_id) @@ -19,7 +20,7 @@ function parse_custom(notebook::Notebook, cell::Cell)::Expr raw = if can_insert_filename filename = pluto_filename(notebook, cell) ex = Base.parse_input_line(cell.code, filename=filename) - if (ex isa Expr) && (ex.head == :toplevel) + if Meta.isexpr(ex, :toplevel) # if there is more than one expression: if count(a -> !(a isa LineNumberNode), ex.args) > 1 Expr(:error, "extra token after end of expression\n\nBoundaries: $(expression_boundaries(cell.code))") @@ -100,12 +101,14 @@ Make some small adjustments to the `expr` to make it work nicely inside a timed, 3. If `expr` is a `:(=)` expression with a curly assignment, wrap it in a `:const` to allow execution - see https://github.com/fonsp/Pluto.jl/issues/517 """ function preprocess_expr(expr::Expr) - if expr.head == :toplevel + if expr.head === :toplevel Expr(:block, expr.args...) - elseif expr.head == :module + elseif expr.head === :module Expr(:toplevel, expr) - elseif expr.head == :(=) && (expr.args[1] isa Expr && expr.args[1].head == :curly) + elseif expr.head === :(=) && (expr.args[1] isa Expr && expr.args[1].head == :curly) Expr(:const, expr) + elseif expr.head === :incomplete + Expr(:call, :(PlutoRunner.throw_syntax_error), expr.args...) else expr end diff --git a/src/analysis/is_just_text.jl b/src/analysis/is_just_text.jl index f4402a6b2a..38339f68af 100644 --- a/src/analysis/is_just_text.jl +++ b/src/analysis/is_just_text.jl @@ -1,12 +1,22 @@ -const md_and_friends = [Symbol("@md_str"), Symbol("@html_str"), :getindex] +const md_and_friends = [ + # Text + Symbol("@md_str"), + Symbol("@html_str"), + :getindex, +] """Does the cell only contain md"..." and html"..."? This is used to run these cells first.""" function is_just_text(topology::NotebookTopology, cell::Cell)::Bool # https://github.com/fonsp/Pluto.jl/issues/209 - isempty(topology.nodes[cell].definitions) && isempty(topology.nodes[cell].funcdefs_with_signatures) && - topology.nodes[cell].references ⊆ md_and_friends && + node = topology.nodes[cell] + ((isempty(node.definitions) && + isempty(node.funcdefs_with_signatures) && + node.references ⊆ md_and_friends) || + (length(node.references) == 2 && + :PlutoRunner in node.references && + Symbol("PlutoRunner.throw_syntax_error") in node.references)) && no_loops(ExpressionExplorer.maybe_macroexpand(topology.codes[cell].parsedcode; recursive=true)) end @@ -24,4 +34,4 @@ function no_loops(ex::Expr) end end -no_loops(x) = true \ No newline at end of file +no_loops(x) = true diff --git a/src/runner/PlutoRunner.jl b/src/runner/PlutoRunner.jl index 6916596b7a..bbd1800e00 100644 --- a/src/runner/PlutoRunner.jl +++ b/src/runner/PlutoRunner.jl @@ -273,7 +273,7 @@ function try_macroexpand(mod::Module, notebook_id::UUID, cell_id::UUID, expr; ca pop!(cell_expanded_exprs, cell_id, nothing) # Remove toplevel block, as that screws with the computer and everything - expr_not_toplevel = if expr.head == :toplevel || expr.head == :block + expr_not_toplevel = if Meta.isexpr(expr, (:toplevel, :block)) Expr(:block, expr.args...) else @warn "try_macroexpand expression not :toplevel or :block" expr @@ -1030,7 +1030,177 @@ format_output(::Nothing; context=default_iocontext) = ("", MIME"text/plain"()) "Downstream packages can set this to false to obtain unprettified stack traces." const PRETTY_STACKTRACES = Ref(true) +# @codemirror/lint has only three levels +function convert_julia_syntax_level(level) + level == :error ? "error" : + level == :warning ? "warning" : "info" +end + +""" + map_byte_range_to_utf16_codepoints(s::String, start_byte::Int, end_byte::Int)::Tuple{Int,Int} + +Taken from `Base.transcode(::Type{UInt16}, src::Vector{UInt8})` +but without line constraints. It also does not support invalid +UTF-8 encoding which `String` should never be anyway. + +This maps the given raw byte range `(start_byte, end_byte)` range to UTF-16 codepoints indices. + +The resulting range can then be used by code-mirror on the frontend, quoting from the code-mirror docs: + +> Character positions are counted from zero, and count each line break and UTF-16 code unit as one unit. + +Examples: +```julia + 123 + vv +julia> map_byte_range_to_utf16_codepoints("abc", 2, 3) +(2, 3) + + 1122 + v v +julia> map_byte_range_to_utf16_codepoints("🍕🍕", 1, 8) +(1, 4) + + 11233 + v v +julia> map_byte_range_to_utf16_codepoints("🍕c🍕", 1, 5) +(1, 3) +``` +""" +function map_byte_range_to_utf16_codepoints(s, start_byte, end_byte) + invalid_utf8() = error("invalid UTF-8 string") + codeunit(s) == UInt8 || invalid_utf8() + + i, n = 1, ncodeunits(s) + u16 = 0 + + from, to = -1, -1 + a = codeunit(s, 1) + while true + if i == start_byte + from = u16 + end + if i == end_byte + to = u16 + break + end + if i < n && -64 <= a % Int8 <= -12 # multi-byte character + i += 1 + b = codeunit(s, i) + if -64 <= (b % Int8) || a == 0xf4 && 0x8f < b + # invalid UTF-8 (non-continuation of too-high code point) + invalid_utf8() + elseif a < 0xe0 # 2-byte UTF-8 + if i == start_byte + from = u16 + end + if i == end_byte + to = u16 + break + end + elseif i < n # 3/4-byte character + i += 1 + c = codeunit(s, i) + if -64 <= (c % Int8) # invalid UTF-8 (non-continuation) + invalid_utf8() + elseif a < 0xf0 # 3-byte UTF-8 + if i == start_byte + from = u16 + end + if i == end_byte + to = u16 + break + end + elseif i < n + i += 1 + d = codeunit(s, i) + if -64 <= (d % Int8) # invalid UTF-8 (non-continuation) + invalid_utf8() + elseif a == 0xf0 && b < 0x90 # overlong encoding + invalid_utf8() + else # 4-byte UTF-8 && 2 codeunits UTF-16 + u16 += 1 + if i == start_byte + from = u16 + end + if i == end_byte + to = u16 + break + end + end + else # too short + invalid_utf8() + end + else # too short + invalid_utf8() + end + else + # ASCII or invalid UTF-8 (continuation byte or too-high code point) + end + u16 += 1 + if i >= n + break + end + i += 1 + a = codeunit(s, i) + end + + if from == -1 + from = u16 + end + if to == -1 + to = u16 + end + + return (from, to) +end + +function convert_diagnostic_to_dict(source, diag) + code = source.code + + # JuliaSyntax uses `last_byte < first_byte` to signal an empty range. + # https://github.com/JuliaLang/JuliaSyntax.jl/blob/97e2825c68e770a3f56f0ec247deda1a8588070c/src/diagnostics.jl#L67-L75 + # it references the byte range as such: `source[first_byte:last_byte]` whereas codemirror + # is non inclusive, therefore we move the `last_byte` to the next valid character in the string, + # an empty range then becomes `from == to`, also JuliaSyntax is one based whereas code-mirror is zero-based + # but this is handled in `map_byte_range_to_utf16_codepoints` with `u16 = 0` initially. + first_byte = min(diag.first_byte, lastindex(code) + 1) + last_byte = min(nextind(code, diag.last_byte), lastindex(code) + 1) + + from, to = map_byte_range_to_utf16_codepoints(code, first_byte, last_byte) + + Dict(:from => from, + :to => to, + :message => diag.message, + :source => "JuliaSyntax.jl", + :line => first(Base.JuliaSyntax.source_location(source, diag.first_byte)), + :severity => convert_julia_syntax_level(diag.level)) +end + +function convert_parse_error_to_dict(ex) + Dict( + :source => ex.source.code, + :diagnostics => [ + convert_diagnostic_to_dict(ex.source, diag) + for diag in ex.diagnostics + ] + ) +end + +function throw_syntax_error(@nospecialize(syntax_err)) + syntax_err isa String && (syntax_err = "syntax: $syntax_err") + syntax_err isa Exception || (syntax_err = ErrorException(syntax_err)) + throw(syntax_err) +end + +const has_julia_syntax = isdefined(Base, :JuliaSyntax) && fieldcount(Base.Meta.ParseError) == 2 + function format_output(val::CapturedException; context=default_iocontext) + if has_julia_syntax && val.ex isa Base.Meta.ParseError && val.ex.detail isa Base.JuliaSyntax.ParseError + dict = convert_parse_error_to_dict(val.ex.detail) + return dict, MIME"application/vnd.pluto.parseerror+object"() + end + stacktrace = if PRETTY_STACKTRACES[] ## We hide the part of the stacktrace that belongs to Pluto's evalling of user code. stack = [s for (s, _) in val.processed_bt] diff --git a/test/Dynamic.jl b/test/Dynamic.jl index 669017f640..56496f0d2d 100644 --- a/test/Dynamic.jl +++ b/test/Dynamic.jl @@ -156,7 +156,7 @@ end let doc_output = Pluto.PlutoRunner.doc_fetcher("sor", Main)[1] @test occursin("Similar results:", doc_output) - @test occursin("sortperm", doc_output) + @test occursin("sort", doc_output) end @test occursin("\\div", Pluto.PlutoRunner.doc_fetcher("÷", Main)[1]) diff --git a/test/ExpressionExplorer.jl b/test/ExpressionExplorer.jl index afe010c1ff..5ab13b83a9 100644 --- a/test/ExpressionExplorer.jl +++ b/test/ExpressionExplorer.jl @@ -1,4 +1,5 @@ using Test +import Pluto: PlutoRunner #= `@test_broken` means that the test doesn't pass right now, but we want it to pass. Feel free to try to fix it and open a PR! @@ -795,3 +796,16 @@ Some of these @test_broken lines are commented out to prevent printing to the te @test :Date ∈ rn.references end end + +@testset "UTF-8 to Codemirror UTF-16 byte mapping" begin + # range ends are non inclusives + tests = [ + (" aaaa", (2, 4), (1, 3)), # cm is zero based + (" 🍕🍕", (2, 6), (1, 3)), # a 🍕 is two UTF16 codeunits + (" 🍕🍕", (6, 10), (3, 5)), # a 🍕 is two UTF16 codeunits + ] + for (s, (start_byte, end_byte), (from, to)) in tests + @show s + @test PlutoRunner.map_byte_range_to_utf16_codepoints(s, start_byte, end_byte) == (from, to) + end +end diff --git a/test/React.jl b/test/React.jl index 191540c53a..0921a4ce0d 100644 --- a/test/React.jl +++ b/test/React.jl @@ -1752,4 +1752,28 @@ import Distributed update_run!(🍭, notebook, notebook.cells) @test all(noerror, notebook.cells) end + + @testset "ParseError messages" begin + notebook = Notebook(Cell.([ + "begin", + "\n\nend", + ])) + update_run!(🍭, notebook, notebook.cells) + @test Pluto.is_just_text(notebook.topology, notebook.cells[1]) + @test Pluto.is_just_text(notebook.topology, notebook.cells[2]) + @static if VERSION >= v"1.10.0-DEV.1548" # ~JuliaSyntax PR Pluto.jl#2526 julia#46372 + @test haskey(notebook.cells[1].output.body, :source) + @test haskey(notebook.cells[1].output.body, :diagnostics) + + @test haskey(notebook.cells[2].output.body, :source) + @test haskey(notebook.cells[2].output.body, :diagnostics) + else + @test !occursinerror("(incomplete ", notebook.cells[1]) + @test !occursinerror("(incomplete ", notebook.cells[2]) + + @show notebook.cells[1].output.body + @test startswith(notebook.cells[1].output.body[:msg], "syntax:") + @test startswith(notebook.cells[2].output.body[:msg], "syntax:") + end + end end diff --git a/test/packages/Basic.jl b/test/packages/Basic.jl index 773be1ec71..7cc83ee9b1 100644 --- a/test/packages/Basic.jl +++ b/test/packages/Basic.jl @@ -648,7 +648,25 @@ import Distributed WorkspaceManager.unmake_workspace((🍭, notebook)) end - + + @testset "PlutoRunner Syntax Error" begin + notebook = Notebook([ + Cell("1 +"), + Cell("PlutoRunner.throw_syntax_error"), + Cell("PlutoRunner.throw_syntax_error(1)"), + ]) + + update_run!(🍭, notebook, notebook.cells) + + @test notebook.cells[1].errored + @test noerror(notebook.cells[2]) + @test notebook.cells[3].errored + + @test Pluto.is_just_text(notebook.topology, notebook.cells[1]) + @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]) + end + @testset "Precompilation" begin compilation_dir = joinpath(DEPOT_PATH[1], "compiled", "v$(VERSION.major).$(VERSION.minor)") @assert isdir(compilation_dir) From 83ec7c59661d672caa7e8be63049c9215bea14cf Mon Sep 17 00:00:00 2001 From: Alberto Mengali Date: Thu, 24 Aug 2023 20:02:48 +0200 Subject: [PATCH 29/52] switch any-point:coarse to pointer:coarse (#2631) The current CSS keeps the hide cell buttons always on when Pluto is opened on laptops which also have a touchscreen. Using pointer instead of any-pointer should only make the button always on only on devices that have the touchscreen as primary. --- frontend/editor.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/editor.css b/frontend/editor.css index 73964d87d5..8a08894962 100644 --- a/frontend/editor.css +++ b/frontend/editor.css @@ -1520,7 +1520,7 @@ pluto-input > button > span { } } -@media screen and (any-pointer: coarse) { +@media screen and (pointer: coarse) { pluto-cell > button, pluto-cell > pluto-runarea { opacity: 0; From 137420c7c8a7187a426503f2cf957c29086c9868 Mon Sep 17 00:00:00 2001 From: jbrea Date: Sun, 27 Aug 2023 16:51:17 +0200 Subject: [PATCH 30/52] fix EmbeddableDisplay (#2632) * fix EmbeddableDisplay * pass cell_id to logs * Revert "pass cell_id to logs" This reverts commit 7829d4c0ff1777e7df3dc787d42d0130bf9ca487. * Add test for `embed_display` --------- Co-authored-by: Paul --- src/runner/PlutoRunner.jl | 2 +- test/RichOutput.jl | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/runner/PlutoRunner.jl b/src/runner/PlutoRunner.jl index bbd1800e00..f1e67a5236 100644 --- a/src/runner/PlutoRunner.jl +++ b/src/runner/PlutoRunner.jl @@ -2382,7 +2382,7 @@ function Base.show(io::IO, m::MIME"text/html", e::EmbeddableDisplay) // see https://plutocon2021-demos.netlify.app/fonsp%20%E2%80%94%20javascript%20inside%20pluto to learn about the techniques used in this script - const body = $(publish_to_js(body, e.script_id)); + const body = $(PublishedToJavascript(body)); const mime = "$(string(mime))"; const create_new = this == null || this._mime !== mime; diff --git a/test/RichOutput.jl b/test/RichOutput.jl index 1132472112..69e1eccff9 100644 --- a/test/RichOutput.jl +++ b/test/RichOutput.jl @@ -229,6 +229,21 @@ import Pluto: update_run!, WorkspaceManager, ClientSession, ServerSession, Noteb end end + @testset "embed_display" begin + 🍭.options.evaluation.workspace_use_distributed = false + notebook = Notebook([ + Cell("x = randn(10)"), + Cell(raw"md\"x = $(embed_display(x))\"") + ]) + update_run!(🍭, notebook, notebook.cells) + + @test notebook.cells[1] |> noerror + @test notebook.cells[2] |> noerror + + @test notebook.cells[2].output.body isa String + @test occursin("getPublishedObject", notebook.cells[2].output.body) + end + @testset "Table viewer" begin 🍭.options.evaluation.workspace_use_distributed = true notebook = Notebook([ From fb1f1a5dc4e7ae35f6611c7f09203b87c57868ce Mon Sep 17 00:00:00 2001 From: Paul Berg Date: Tue, 29 Aug 2023 23:28:46 +0200 Subject: [PATCH 31/52] Limit prefix length (#2636) --- src/runner/PlutoRunner.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runner/PlutoRunner.jl b/src/runner/PlutoRunner.jl index f1e67a5236..030de8cd48 100644 --- a/src/runner/PlutoRunner.jl +++ b/src/runner/PlutoRunner.jl @@ -1422,7 +1422,7 @@ function array_prefix(@nospecialize(x::Vector{<:Any})) end function array_prefix(@nospecialize(x)) - original = sprint(Base.showarg, x, false) + original = sprint(Base.showarg, x, false; context=:limit => true) string(lstrip(original, ':'), ": ")::String end From 17d105b60282038f2194862109fab2afc5730a13 Mon Sep 17 00:00:00 2001 From: Vlad Flore Date: Sun, 3 Sep 2023 17:29:24 +0200 Subject: [PATCH 32/52] change selector so that CSS applies correctly (#2637) --- frontend/editor.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/editor.css b/frontend/editor.css index 8a08894962..6cf6bf7617 100644 --- a/frontend/editor.css +++ b/frontend/editor.css @@ -2665,7 +2665,7 @@ footer button { font-size: 0.75rem; } -#info { +footer #info { max-width: 9400px; margin: 0 auto; padding: 1rem; From 5b96b85f1a2500dcefb0a739399f77edf5ae78d6 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Wed, 6 Sep 2023 22:36:00 +0200 Subject: [PATCH 33/52] Warn when including PasswordField content in an html export --- frontend/components/ExportBanner.js | 66 +++++++++++++++++++---------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/frontend/components/ExportBanner.js b/frontend/components/ExportBanner.js index 0c1bac5383..2188763d9b 100644 --- a/frontend/components/ExportBanner.js +++ b/frontend/components/ExportBanner.js @@ -26,8 +26,23 @@ const Square = ({ fill }) => html` ` -//@ts-ignore -window.enable_secret_pluto_recording = true +export const WarnForVisisblePasswords = () => { + if ( + Array.from(document.querySelectorAll("bond")).some((bond_el) => + Array.from(bond_el.querySelectorAll(`input[type="password"]`)).some((input) => { + // @ts-ignore + if (input?.value !== "") { + input.scrollIntoView() + return true + } + }) + ) + ) { + alert( + "Warning: this notebook includes a password input with something typed in it. The contents of this password field will be included in the exported file in an unsafe way. \n\nClear the password field and export again to avoid this problem." + ) + } +} export const ExportBanner = ({ notebook_id, onClose, notebookfile_url, notebookexport_url, start_recording }) => { // @ts-ignore @@ -49,7 +64,16 @@ export const ExportBanner = ({ notebook_id, onClose, notebookfile_url, notebooke
<${Triangle} fill="#a270ba" /> Notebook file
Download a copy of the .jl script.
- exportNotebook(e, 1)}> + { + WarnForVisisblePasswords() + exportNotebook(e, 1) + }} + >
<${Square} fill="#E86F51" /> Static HTML
An .html file for your web page, or to share online.
@@ -57,26 +81,22 @@ export const ExportBanner = ({ notebook_id, onClose, notebookfile_url, notebooke
<${Square} fill="#619b3d" /> PDF
A static .pdf file for print or email.
- ${ - //@ts-ignore - window.enable_secret_pluto_recording - ? html` -
record
- { - start_recording() - onClose() - e.preventDefault() - }} - class="export_card" - > -
<${Circle} fill="#E86F51" /> Record (preview)
-
Capture the entire notebook, and any changes you make.
-
- ` - : null - } + ${html` +
record
+ { + WarnForVisisblePasswords() + start_recording() + onClose() + e.preventDefault() + }} + class="export_card" + > +
<${Circle} fill="#E86F51" /> Record (preview)
+
Capture the entire notebook, and any changes you make.
+
+ `}
- + + ` } From 89037c80cc0a92f9c14e1bbbfe8974383cd949f6 Mon Sep 17 00:00:00 2001 From: Markus Kuhn Date: Mon, 18 Sep 2023 12:16:59 +0200 Subject: [PATCH 45/52] Display banner from Pluto.__init__, not during precompile (#2628) Co-authored-by: Fons van der Plas --- Project.toml | 2 ++ src/Pluto.jl | 36 ++++++++++++++++++++++++++---------- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/Project.toml b/Project.toml index 7c7f655297..ffb77d4640 100644 --- a/Project.toml +++ b/Project.toml @@ -25,6 +25,7 @@ PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" RegistryInstances = "2792f1a3-b283-48e8-9a74-f99dce5104f3" RelocatableFolders = "05181044-ff0b-4ac5-8273-598c1e38db00" +Scratch = "6c6a2e73-6563-6170-7368-637461726353" Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" @@ -43,6 +44,7 @@ PrecompileSignatures = "3" PrecompileTools = "1" RegistryInstances = "0.1" RelocatableFolders = "0.1, 0.2, 0.3, 1" +Scratch = "1.1" Tables = "1" URIs = "1.3" julia = "^1.6" diff --git a/src/Pluto.jl b/src/Pluto.jl index 2cb0eee2ad..8898d058f2 100644 --- a/src/Pluto.jl +++ b/src/Pluto.jl @@ -33,6 +33,7 @@ function project_relative_path(root, xs...) end import Pkg +import Scratch include_dependency("../Project.toml") const PLUTO_VERSION = VersionNumber(Pkg.TOML.parsefile(joinpath(ROOT_DIR, "Project.toml"))["version"]) @@ -99,16 +100,31 @@ export activate_notebook_environment include("./precompile.jl") -if get(ENV, "JULIA_PLUTO_SHOW_BANNER", "1") != "0" && get(ENV, "CI", "🍄") != "true" -@info """\n - Welcome to Pluto $(PLUTO_VERSION_STR) 🎈 - Start a notebook server using: - - julia> Pluto.run() - - Have a look at the FAQ: - https://github.com/fonsp/Pluto.jl/wiki -\n""" +function __init__() + # Print a welcome banner + if (get(ENV, "JULIA_PLUTO_SHOW_BANNER", "1") != "0" && + get(ENV, "CI", "🍄") != "true" && isinteractive()) + # Print the banner only once per version, if there isn't + # yet a file for this version in banner_shown scratch space. + # (Using the Pluto version as the filename enables later + # version-specific "what's new" messages.) + fn = joinpath(Scratch.@get_scratch!("banner_shown"), PLUTO_VERSION_STR) + if !isfile(fn) + @info """ + + Welcome to Pluto $(PLUTO_VERSION_STR) 🎈 + Start a notebook server using: + + julia> Pluto.run() + + Have a look at the FAQ: + https://github.com/fonsp/Pluto.jl/wiki + + """ + # create empty file to indicate that we've shown the banner + write(fn, ""); + end + end end end From 06589c782994d8233e29c01742ac69c361c0d0aa Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Mon, 18 Sep 2023 14:08:30 +0200 Subject: [PATCH 46/52] minor TS type fixes --- frontend/components/Editor.js | 8 ++++---- frontend/components/SelectionArea.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/components/Editor.js b/frontend/components/Editor.js index 50f4326a71..99bcc2369f 100644 --- a/frontend/components/Editor.js +++ b/frontend/components/Editor.js @@ -1185,13 +1185,13 @@ patch: ${JSON.stringify( // if (e.defaultPrevented) { // return // } - if (e.key.toLowerCase() === "q" && has_ctrl_or_cmd_pressed(e)) { + if (e.key?.toLowerCase() === "q" && has_ctrl_or_cmd_pressed(e)) { // This one can't be done as cmd+q on mac, because that closes chrome - Dral if (Object.values(this.state.notebook.cell_results).some((c) => c.running || c.queued)) { this.actions.interrupt_remote() } e.preventDefault() - } else if (e.key.toLowerCase() === "s" && has_ctrl_or_cmd_pressed(e)) { + } else if (e.key?.toLowerCase() === "s" && has_ctrl_or_cmd_pressed(e)) { const some_cells_ran = this.actions.set_and_run_all_changed_remote_cells() if (!some_cells_ran) { // all cells were in sync allready @@ -1239,8 +1239,8 @@ patch: ${JSON.stringify( } if (this.state.disable_ui && this.state.backend_launch_phase === BackendLaunchPhase.wait_for_user) { - // const code = e.key.charCodeAt(0) - if (e.key === "Enter" || e.key.length === 1) { + // const code = e.key?.charCodeAt(0) + if (e.key === "Enter" || e.key?.length === 1) { if (!document.body.classList.contains("wiggle_binder")) { document.body.classList.add("wiggle_binder") setTimeout(() => { diff --git a/frontend/components/SelectionArea.js b/frontend/components/SelectionArea.js index 7f145bb7dd..eb705ad324 100644 --- a/frontend/components/SelectionArea.js +++ b/frontend/components/SelectionArea.js @@ -151,7 +151,7 @@ export const SelectionArea = ({ on_selection, set_scroller, cell_order }) => { // Ctrl+A to select all cells const onkeydown = (e) => { - if (e.key.toLowerCase() === "a" && has_ctrl_or_cmd_pressed(e)) { + if (e.key?.toLowerCase() === "a" && has_ctrl_or_cmd_pressed(e)) { // if you are not writing text somewhere else if (document.activeElement === document.body && (window.getSelection()?.isCollapsed ?? true)) { on_selection(cell_order) From 690b4012dfaac389df17d276ea4b4593c86fd90b Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Mon, 18 Sep 2023 14:20:37 +0200 Subject: [PATCH 47/52] Frontmatter GUI: multiple authors (#2650) --- frontend/components/FrontmatterInput.js | 125 ++++++++++++++++++------ frontend/editor.css | 4 + 2 files changed, 101 insertions(+), 28 deletions(-) diff --git a/frontend/components/FrontmatterInput.js b/frontend/components/FrontmatterInput.js index 3ca04e3d24..a60f4b5b6a 100644 --- a/frontend/components/FrontmatterInput.js +++ b/frontend/components/FrontmatterInput.js @@ -1,10 +1,12 @@ import { html, Component, useRef, useLayoutEffect, useState, useEffect } from "../imports/Preact.js" import { has_ctrl_or_cmd_pressed } from "../common/KeyboardShortcuts.js" +import _ from "../imports/lodash.js" import "https://cdn.jsdelivr.net/gh/fonsp/rebel-tag-input@1.0.6/lib/rebel-tag-input.mjs" //@ts-ignore import dialogPolyfill from "https://cdn.jsdelivr.net/npm/dialog-polyfill@0.5.6/dist/dialog-polyfill.esm.min.js" +import immer from "../imports/immer.js" /** * @param {{ @@ -23,9 +25,12 @@ export const FrontMatterInput = ({ remote_frontmatter, set_remote_frontmatter }) // console.log("New frontmatter:", frontmatter) // }, [frontmatter]) - const fm_setter = (key) => (value) => { - set_frontmatter((fm) => ({ ...fm, [key]: value })) - } + const fm_setter = (key) => (value) => + set_frontmatter( + immer((fm) => { + _.set(fm, key, value) + }) + ) const dialog_ref = useRef(/** @type {HTMLDialogElement?} */ (null)) useLayoutEffect(() => { @@ -42,10 +47,26 @@ export const FrontMatterInput = ({ remote_frontmatter, set_remote_frontmatter }) close() } const submit = () => { - set_remote_frontmatter(frontmatter).then(() => alert("Frontmatter synchronized ✔\n\nThese parameters will be used in future exports.")) + set_remote_frontmatter(clean_data(frontmatter) ?? {}).then(() => + alert("Frontmatter synchronized ✔\n\nThese parameters will be used in future exports.") + ) close() } + const clean_data = (obj) => { + let a = _.isPlainObject(obj) + ? Object.fromEntries( + Object.entries(obj) + .map(([key, val]) => [key, clean_data(val)]) + .filter(([key, val]) => val != null) + ) + : _.isArray(obj) + ? obj.map(clean_data).filter((x) => x != null) + : obj + + return _.isEmpty(a) ? null : a + } + useLayoutEffect(() => { window.addEventListener("open pluto frontmatter", open) return () => { @@ -68,43 +89,91 @@ export const FrontMatterInput = ({ remote_frontmatter, set_remote_frontmatter }) description: null, date: null, tags: [], + author: [{}], ...frontmatter, } - return html` -

Frontmatter

-

- If you are publishing this notebook on the web, you can set the parameters below to provide HTML metadata. This is useful for search engines and - social media. -

-
- ${Object.entries(frontmatter_with_defaults).map(([key, value]) => { - let id = `fm-${key}` - return html` - - <${Input} type=${field_type(key)} id=${id} value=${value} on_value=${fm_setter(key)} /> - - ` - })} + const show_entry = ([key, value]) => !((_.isArray(value) && field_type(key) !== "tags") || _.isPlainObject(value)) + + const entries_input = (data, base_path) => { + return html` + ${Object.entries(data) + .filter(show_entry) + .map(([key, value]) => { + let path = `${base_path}${key}` + let id = `fm-${path}` + return html` + + <${Input} type=${field_type(key)} id=${id} value=${value} on_value=${fm_setter(path)} /> + + ` + })} + ` + } + + return html` +

Frontmatter

+

+ If you are publishing this notebook on the web, you can set the parameters below to provide HTML metadata. This is useful for search engines and + social media. +

+
+ ${entries_input(frontmatter_with_defaults, ``)} + ${!_.isArray(frontmatter_with_defaults.author) + ? null + : frontmatter_with_defaults.author.map((author, i) => { + let author_with_defaults = { + name: null, + url: null, + ...author, + } + + return html` +
+ Author ${i + 1} + + ${entries_input(author_with_defaults, `author[${i}].`)} +
+ ` + })} + ${!_.isArray(frontmatter_with_defaults.author) + ? null + : html``}
diff --git a/frontend/editor.css b/frontend/editor.css index 7d92b004a4..2f6b6b2e4a 100644 --- a/frontend/editor.css +++ b/frontend/editor.css @@ -3445,6 +3445,10 @@ pluto-cell.hooked_up pluto-output { margin-top: 0.5em; } +.pluto-frontmatter fieldset { + grid-column: 1/4; +} + .pluto-frontmatter .final { display: flex; margin-top: 2rem; From 03bbc9aa6785bb8ce7d35d64852ebf3efb775ba3 Mon Sep 17 00:00:00 2001 From: Connor Burns Date: Mon, 18 Sep 2023 06:21:56 -0600 Subject: [PATCH 48/52] Slider server stuck loading on staterequest failure (#2649) --- frontend/common/SliderServerClient.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/frontend/common/SliderServerClient.js b/frontend/common/SliderServerClient.js index e2a8933619..f93f46efd3 100644 --- a/frontend/common/SliderServerClient.js +++ b/frontend/common/SliderServerClient.js @@ -82,11 +82,15 @@ export const slider_server_actions = ({ setStatePromise, launch_params, actions, const dep_graph = get_current_state().cell_dependencies const starts = get_starts(dep_graph, bonds_to_set.current) const running_cells = [...recursive_dependencies(dep_graph, starts)] - await setStatePromise( - immer((state) => { - running_cells.forEach((cell_id) => (state.notebook.cell_results[cell_id][starts.has(cell_id) ? "running" : "queued"] = true)) - }) - ) + + const update_cells_running = async (running) => + await setStatePromise( + immer((state) => { + running_cells.forEach((cell_id) => (state.notebook.cell_results[cell_id][starts.has(cell_id) ? "running" : "queued"] = running)) + }) + ) + + await update_cells_running(true) if (bonds_to_set.current.size > 0) { const to_send = new Set(bonds_to_set.current) @@ -129,14 +133,13 @@ export const slider_server_actions = ({ setStatePromise, launch_params, actions, ids_of_cells_that_ran.forEach((id) => { state.cell_results[id] = original.cell_results[id] }) - running_cells.forEach((id) => { - state.cell_results[id].queued = false - state.cell_results[id].running = false - }) })(get_current_state()) ) } catch (e) { console.error(unpacked, e) + } finally { + // reset cell running state regardless of request outcome + await update_cells_running(false) } } }) From a7a424c0412399b6057bb505194d01f2f2ea6131 Mon Sep 17 00:00:00 2001 From: "Sergio A. Vargas" Date: Mon, 18 Sep 2023 08:42:23 -0500 Subject: [PATCH 49/52] Replace Distributed with Malt soon! (#2240) Co-authored-by: Fons van der Plas Co-authored-by: Panagiotis Georgakopoulos Co-authored-by: Paul Berg --- Project.toml | 3 +- src/Configuration.jl | 5 + src/evaluation/WorkspaceManager.jl | 254 +++++++++++++---------------- src/runner/PlutoRunner.jl | 29 ++-- src/webserver/REPLTools.jl | 23 ++- test/Bonds.jl | 11 +- test/Dynamic.jl | 2 +- test/React.jl | 25 ++- test/ReloadFromFile.jl | 1 - test/WorkspaceManager.jl | 17 +- test/cell_disabling.jl | 4 +- test/helpers.jl | 7 +- test/packages/Basic.jl | 36 ++-- test/runtests.jl | 4 +- 14 files changed, 209 insertions(+), 212 deletions(-) diff --git a/Project.toml b/Project.toml index ffb77d4640..7292cf6847 100644 --- a/Project.toml +++ b/Project.toml @@ -8,7 +8,6 @@ version = "0.19.27" Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" Configurations = "5218b696-f38b-4ac9-8b61-a12ec717816d" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" -Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" FileWatching = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" FuzzyCompletions = "fb4132e2-a121-4a70-b8a1-d5b831dcdcc2" HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" @@ -17,6 +16,7 @@ InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" LoggingExtras = "e6f89c97-d47a-5376-807f-9c37f3926c36" MIMEs = "6c6e2e6c-3030-632d-7369-2d6c69616d65" +Malt = "36869731-bdee-424d-aa32-cab38c994e3b" Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" MsgPack = "99f44e22-a591-53d1-9472-aa23ef4bd671" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" @@ -39,6 +39,7 @@ HTTP = "^1.5.2" HypertextLiteral = "0.7, 0.8, 0.9" LoggingExtras = "0.4, 1" MIMEs = "0.1" +Malt = "1.0.3" MsgPack = "1.1" PrecompileSignatures = "3" PrecompileTools = "1" diff --git a/src/Configuration.jl b/src/Configuration.jl index 95236fa6b4..2b7eed6ae9 100644 --- a/src/Configuration.jl +++ b/src/Configuration.jl @@ -140,6 +140,7 @@ end const RUN_NOTEBOOK_ON_LOAD_DEFAULT = true const WORKSPACE_USE_DISTRIBUTED_DEFAULT = true +const WORKSPACE_USE_DISTRIBUTED_STDLIB_DEFAULT = nothing const LAZY_WORKSPACE_CREATION_DEFAULT = false const CAPTURE_STDOUT_DEFAULT = true const WORKSPACE_CUSTOM_STARTUP_EXPR_DEFAULT = nothing @@ -152,6 +153,7 @@ These options are not intended to be changed during normal use. - `run_notebook_on_load::Bool = $RUN_NOTEBOOK_ON_LOAD_DEFAULT` Whether to evaluate a notebook on load. - `workspace_use_distributed::Bool = $WORKSPACE_USE_DISTRIBUTED_DEFAULT` Whether to start notebooks in a separate process. +- `workspace_use_distributed_stdlib::Bool? = $WORKSPACE_USE_DISTRIBUTED_STDLIB_DEFAULT` Should we use the Distributed stdlib to run processes? Distributed will be replaced by Malt.jl, you can use this option to already get the old behaviour. `nothing` means: determine automatically (which is currently the same as `true`). - `lazy_workspace_creation::Bool = $LAZY_WORKSPACE_CREATION_DEFAULT` - `capture_stdout::Bool = $CAPTURE_STDOUT_DEFAULT` - `workspace_custom_startup_expr::Union{Nothing,Expr} = $WORKSPACE_CUSTOM_STARTUP_EXPR_DEFAULT` An expression to be evaluated in the workspace process before running notebook code. @@ -159,6 +161,7 @@ These options are not intended to be changed during normal use. @option mutable struct EvaluationOptions run_notebook_on_load::Bool = RUN_NOTEBOOK_ON_LOAD_DEFAULT workspace_use_distributed::Bool = WORKSPACE_USE_DISTRIBUTED_DEFAULT + workspace_use_distributed_stdlib::Union{Bool,Nothing} = WORKSPACE_USE_DISTRIBUTED_STDLIB_DEFAULT lazy_workspace_creation::Bool = LAZY_WORKSPACE_CREATION_DEFAULT capture_stdout::Bool = CAPTURE_STDOUT_DEFAULT workspace_custom_startup_expr::Union{Nothing,Expr} = WORKSPACE_CUSTOM_STARTUP_EXPR_DEFAULT @@ -292,6 +295,7 @@ function from_flat_kwargs(; run_notebook_on_load::Bool = RUN_NOTEBOOK_ON_LOAD_DEFAULT, workspace_use_distributed::Bool = WORKSPACE_USE_DISTRIBUTED_DEFAULT, + workspace_use_distributed_stdlib::Union{Bool,Nothing} = WORKSPACE_USE_DISTRIBUTED_STDLIB_DEFAULT, lazy_workspace_creation::Bool = LAZY_WORKSPACE_CREATION_DEFAULT, capture_stdout::Bool = CAPTURE_STDOUT_DEFAULT, workspace_custom_startup_expr::Union{Nothing,Expr} = WORKSPACE_CUSTOM_STARTUP_EXPR_DEFAULT, @@ -340,6 +344,7 @@ function from_flat_kwargs(; evaluation = EvaluationOptions(; run_notebook_on_load, workspace_use_distributed, + workspace_use_distributed_stdlib, lazy_workspace_creation, capture_stdout, workspace_custom_startup_expr, diff --git a/src/evaluation/WorkspaceManager.jl b/src/evaluation/WorkspaceManager.jl index 55fb090655..0d952a7d55 100644 --- a/src/evaluation/WorkspaceManager.jl +++ b/src/evaluation/WorkspaceManager.jl @@ -7,18 +7,19 @@ import ..Pluto.PkgCompat import ..Configuration: CompilerOptions, _merge_notebook_compiler_options, _convert_to_flags import ..Pluto.ExpressionExplorer: FunctionName import ..PlutoRunner -import Distributed +import Malt +import Malt.Distributed """ -Contains the Julia process (in the sense of `Distributed.addprocs`) to evaluate code in. +Contains the Julia process to evaluate code in. Each notebook gets at most one `Workspace` at any time, but it can also have no `Workspace` (it cannot `eval` code in this case). """ Base.@kwdef mutable struct Workspace - pid::Integer + worker::Malt.AbstractWorker notebook_id::UUID discarded::Bool=false - remote_log_channel::Distributed.RemoteChannel + remote_log_channel::Union{Distributed.RemoteChannel,AbstractChannel} module_name::Symbol dowork_token::Token=Token() nbpkg_was_active::Bool=false @@ -29,25 +30,19 @@ end const SN = Tuple{ServerSession, Notebook} +"These expressions get evaluated whenever a new `Workspace` process is created." +process_preamble() = quote + Base.exit_on_sigint(false) + include($(project_relative_path(joinpath("src", "runner"), "Loader.jl"))) + ENV["GKSwstype"] = "nul" + ENV["JULIA_REVISE_WORKER_ONLY"] = "1" +end + const active_workspaces = Dict{UUID,Task}() "Set of notebook IDs that we will never make a process for again." const discarded_workspaces = Set{UUID}() -const Distributed_expr = quote - Base.loaded_modules[Base.PkgId(Base.UUID("8ba89e20-285c-5b6f-9357-94700520ee1b"), "Distributed")] -end - -"These expressions get evaluated whenever a new `Workspace` process is created." -function process_preamble() - quote - Base.exit_on_sigint(false) - include($(project_relative_path(joinpath("src", "runner"), "Loader.jl"))) - ENV["GKSwstype"] = "nul" - ENV["JULIA_REVISE_WORKER_ONLY"] = "1" - end -end - "Create a workspace for the notebook, optionally in the main process." function make_workspace((session, notebook)::SN; is_offline_renderer::Bool=false)::Workspace workspace_business = is_offline_renderer ? Status.Business(name=:gobble) : Status.report_business_started!(notebook.status_tree, :workspace) @@ -55,26 +50,22 @@ function make_workspace((session, notebook)::SN; is_offline_renderer::Bool=false Status.report_business_planned!(workspace_business, :init_process) is_offline_renderer || (notebook.process_status = ProcessStatus.starting) - - use_distributed = !is_offline_renderer && session.options.evaluation.workspace_use_distributed - - pid = if use_distributed - @debug "Creating workspace process" notebook.path length(notebook.cells) - create_workspaceprocess(; - compiler_options=_merge_notebook_compiler_options(notebook, session.options.compiler), - status=create_status, - ) + + WorkerType = if is_offline_renderer || !session.options.evaluation.workspace_use_distributed + Malt.InProcessWorker + elseif something( + session.options.evaluation.workspace_use_distributed_stdlib, + true + # VERSION < v"1.8.0-0" + ) + Malt.DistributedStdlibWorker else - pid = Distributed.myid() - if !(isdefined(Main, :PlutoRunner) && Main.PlutoRunner isa Module) - # Make PlutoRunner available in Main, right now it's only defined inside this Pluto module. - @eval Main begin - PlutoRunner = $(PlutoRunner) - end - end - pid + Malt.Worker 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)) @@ -82,33 +73,29 @@ function make_workspace((session, notebook)::SN; is_offline_renderer::Bool=false Status.report_business_planned!(init_status, Symbol(3)) Status.report_business_planned!(init_status, Symbol(4)) - Distributed.remotecall_eval(Main, [pid], session.options.evaluation.workspace_custom_startup_expr) + Malt.remote_eval_wait(worker, session.options.evaluation.workspace_custom_startup_expr) - Distributed.remotecall_eval(Main, [pid], quote + Malt.remote_eval_wait(worker, quote PlutoRunner.notebook_id[] = $(notebook.notebook_id) end) - remote_log_channel = Core.eval(Main, quote - $(Distributed).RemoteChannel(() -> eval(quote - channel = Channel{Any}(10) - Main.PlutoRunner.setup_plutologger( - $($(notebook.notebook_id)), - channel, - ) + remote_log_channel = Malt.worker_channel(worker, quote + channel = Channel{Any}(10) + Main.PlutoRunner.setup_plutologger( + $(notebook.notebook_id), channel - end), $pid) + ) + channel end) - run_channel = Core.eval(Main, quote - $(Distributed).RemoteChannel(() -> eval(:(Main.PlutoRunner.run_channel)), $pid) - end) + run_channel = Malt.worker_channel(worker, :(Main.PlutoRunner.run_channel)) - module_name = create_emptyworkspacemodule(pid) + module_name = create_emptyworkspacemodule(worker) - original_LOAD_PATH, original_ACTIVE_PROJECT = Distributed.remotecall_eval(Main, pid, :(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(; - pid, + worker, notebook_id=notebook.notebook_id, remote_log_channel, module_name, @@ -155,20 +142,20 @@ function use_nbpkg_environment((session, notebook)::SN, workspace=nothing) workspace.discarded && return workspace.nbpkg_was_active = enabled - if workspace.pid != Distributed.myid() - new_LP = enabled ? ["@", "@stdlib"] : workspace.original_LOAD_PATH - new_AP = enabled ? PkgCompat.env_dir(notebook.nbpkg_ctx) : workspace.original_ACTIVE_PROJECT - - Distributed.remotecall_eval(Main, [workspace.pid], quote - copy!(LOAD_PATH, $(new_LP)) - Base.ACTIVE_PROJECT[] = $(new_AP) - end) - else - # TODO + if workspace.worker isa Malt.InProcessWorker + # Not supported + return end + new_LP = enabled ? ["@", "@stdlib"] : workspace.original_LOAD_PATH + new_AP = enabled ? PkgCompat.env_dir(notebook.nbpkg_ctx) : workspace.original_ACTIVE_PROJECT + + Malt.remote_eval_wait(workspace.worker, quote + copy!(LOAD_PATH, $(new_LP)) + Base.ACTIVE_PROJECT[] = $(new_AP) + end) end -function start_relaying_self_updates((session, notebook)::SN, run_channel::Distributed.RemoteChannel) +function start_relaying_self_updates((session, notebook)::SN, run_channel) while true try next_run_uuid = take!(run_channel) @@ -184,7 +171,7 @@ function start_relaying_self_updates((session, notebook)::SN, run_channel::Distr end end -function start_relaying_logs((session, notebook)::SN, log_channel::Distributed.RemoteChannel) +function start_relaying_logs((session, notebook)::SN, log_channel) update_throttled, flush_throttled = Pluto.throttled(0.1) do Pluto.send_notebook_changes!(Pluto.ClientRequest(; session, notebook)) end @@ -255,7 +242,7 @@ end function bump_workspace_module(session_notebook::SN) workspace = get_workspace(session_notebook) old_name = workspace.module_name - new_name = workspace.module_name = create_emptyworkspacemodule(workspace.pid) + new_name = workspace.module_name = create_emptyworkspacemodule(workspace.worker) old_name, new_name end @@ -263,21 +250,21 @@ end function get_bond_names(session_notebook::SN, cell_id) workspace = get_workspace(session_notebook) - Distributed.remotecall_eval(Main, workspace.pid, quote - PlutoRunner.get_bond_names($cell_id) + Malt.remote_eval_fetch(workspace.worker, quote + PlutoRunner.get_bond_names($cell_id) end) end function possible_bond_values(session_notebook::SN, n::Symbol; get_length::Bool=false) workspace = get_workspace(session_notebook) - Distributed.remotecall_eval(Main, workspace.pid, quote + Malt.remote_eval_fetch(workspace.worker, quote PlutoRunner.possible_bond_values($(QuoteNode(n)); get_length=$(get_length)) end) end -function create_emptyworkspacemodule(pid::Integer)::Symbol - Distributed.remotecall_eval(Main, pid, quote +function create_emptyworkspacemodule(worker::Malt.AbstractWorker)::Symbol + Malt.remote_eval_fetch(worker, quote PlutoRunner.increment_current_module() end) end @@ -285,33 +272,41 @@ end # NOTE: this function only start a worker process using given # compiler options, it does not resolve paths for notebooks # compiler configurations passed to it should be resolved before this -function create_workspaceprocess(; compiler_options=CompilerOptions(), status::Status.Business=Business())::Integer - - Status.report_business_started!(status, Symbol(1)) - Status.report_business_planned!(status, Symbol(2)) - # run on proc 1 in case Pluto is being used inside a notebook process - # Workaround for "only process 1 can add/remove workers" - pid = Distributed.remotecall_eval(Main, 1, quote - $(Distributed_expr).addprocs(1; exeflags=$(_convert_to_flags(compiler_options))) |> first - end) - - Status.report_business_finished!(status, Symbol(1)) - Status.report_business_started!(status, Symbol(2)) - - Distributed.remotecall_eval(Main, [pid], process_preamble()) +function create_workspaceprocess(WorkerType; compiler_options=CompilerOptions(), status::Status.Business=Status.Business())::Malt.AbstractWorker - # so that we NEVER break the workspace with an interrupt 🤕 - @async Distributed.remotecall_eval(Main, [pid], quote - while true - try - wait() - catch end + if WorkerType === Malt.InProcessWorker + worker = WorkerType() + + if !(isdefined(Main, :PlutoRunner) && Main.PlutoRunner isa Module) + # we make PlutoRunner available in Main, right now it's only defined inside this Pluto module. + Malt.remote_eval_wait(Main, worker, quote + PlutoRunner = $(PlutoRunner) + end) end - end) + else + + Status.report_business_started!(status, Symbol(1)) + Status.report_business_planned!(status, Symbol(2)) + + worker = WorkerType(; exeflags=_convert_to_flags(compiler_options)) + + Status.report_business_finished!(status, Symbol(1)) + Status.report_business_started!(status, Symbol(2)) + + Malt.remote_eval_wait(worker, process_preamble()) + + # so that we NEVER break the workspace with an interrupt 🤕 + Malt.remote_eval(worker, quote + while true + try + wait() + catch end + end + end) + end Status.report_business_finished!(status) - - pid + worker end """ @@ -348,28 +343,16 @@ function unmake_workspace(session_notebook::SN; async::Bool=false, verbose::Bool workspace.discarded = true allow_restart || push!(discarded_workspaces, notebook.notebook_id) - if workspace.pid != Distributed.myid() - filter!(p -> fetch(p.second).pid != workspace.pid, active_workspaces) - t = @async begin - interrupt_workspace(workspace; verbose=false) - # run on proc 1 in case Pluto is being used inside a notebook process - # Workaround for "only process 1 can add/remove workers" - Distributed.remotecall_eval(Main, 1, quote - $(Distributed_expr).rmprocs($(workspace.pid)) - end) - end - async || wait(t) - else - if !isready(workspace.dowork_token) - @error "Cannot unmake a workspace running inside the same process: the notebook is still running." - elseif verbose - @warn "Cannot unmake a workspace running inside the same process: the notebook might still be running. If you are sure that your code is not running the notebook async, then you can use the `verbose=false` keyword argument to disable this message." - end + filter!(p -> fetch(p.second).worker != workspace.worker, active_workspaces) + t = @async begin + interrupt_workspace(workspace; verbose=false) + Malt.stop(workspace.worker) end + async || wait(t) nothing end -function distributed_exception_result(ex::Base.IOError, workspace::Workspace) +function workspace_exception_result(ex::Base.IOError, workspace::Workspace) ( output_formatted=PlutoRunner.format_output(CapturedException(ex, [])), errored=true, @@ -381,12 +364,11 @@ function distributed_exception_result(ex::Base.IOError, workspace::Workspace) ) end -function distributed_exception_result(exs::CompositeException, workspace::Workspace) +function workspace_exception_result(exs::CompositeException, workspace::Workspace) ex = first(exs.exceptions) - if ex isa Distributed.RemoteException && - ex.pid == workspace.pid && - ex.captured.ex isa InterruptException + if ex isa InterruptException || (ex isa Malt.RemoteException && occursin("InterruptException", ex.message)) + @info "Found an interrupt!" ex ( output_formatted=PlutoRunner.format_output(CapturedException(InterruptException(), [])), errored=true, @@ -396,7 +378,7 @@ function distributed_exception_result(exs::CompositeException, workspace::Worksp published_objects=Dict{String,Any}(), has_pluto_hook_features=false, ) - elseif ex isa Distributed.ProcessExitedException + elseif ex isa Malt.TerminatedWorkerException ( output_formatted=PlutoRunner.format_output(CapturedException(exs, [])), errored=true, @@ -440,14 +422,10 @@ function eval_format_fetch_in_workspace( )::PlutoRunner.FormattedCellResult workspace = get_workspace(session_notebook) - - is_on_this_process = workspace.pid == Distributed.myid() + is_on_this_process = workspace.worker isa Malt.InProcessWorker # if multiple notebooks run on the same process, then we need to `cd` between the different notebook paths if session_notebook isa Tuple - if is_on_this_process - cd_workspace(workspace, session_notebook[2].path) - end use_nbpkg_environment(session_notebook, workspace) end @@ -456,8 +434,7 @@ function eval_format_fetch_in_workspace( # A try block (on this process) to catch an InterruptException take!(workspace.dowork_token) early_result = try - # Use [pid] instead of pid to prevent fetching output - Distributed.remotecall_eval(Main, [workspace.pid], quote + Malt.remote_eval_wait(workspace.worker, quote PlutoRunner.run_expression( getfield(Main, $(QuoteNode(workspace.module_name))), $(QuoteNode(expr)), @@ -474,7 +451,7 @@ function eval_format_fetch_in_workspace( catch e # Don't use a `finally` because the token needs to be back asap for the interrupting code to pick it up. put!(workspace.dowork_token) - distributed_exception_result(e, workspace) + workspace_exception_result(e, workspace) end if early_result === nothing @@ -488,7 +465,7 @@ end function eval_in_workspace(session_notebook::Union{SN,Workspace}, expr) workspace = get_workspace(session_notebook) - Distributed.remotecall_eval(Main, [workspace.pid], quote + Malt.remote_eval_wait(workspace.worker, quote Core.eval($(workspace.module_name), $(QuoteNode(expr))) end) nothing @@ -509,7 +486,7 @@ function format_fetch_in_workspace( # we format the cell output on the worker, and fetch the formatted output. withtoken(workspace.dowork_token) do try - Distributed.remotecall_eval(Main, workspace.pid, quote + Malt.remote_eval_fetch(workspace.worker, quote PlutoRunner.formatted_result_of( $(workspace.notebook_id), $cell_id, @@ -521,7 +498,7 @@ function format_fetch_in_workspace( ) end) catch e - distributed_exception_result(CompositeException([e]), workspace) + workspace_exception_result(CompositeException([e]), workspace) end end end @@ -529,7 +506,7 @@ end function collect_soft_definitions(session_notebook::SN, modules::Set{Expr}) workspace = get_workspace(session_notebook) - Distributed.remotecall_eval(Main, workspace.pid, quote + Malt.remote_eval_fetch(workspace.worker, quote PlutoRunner.collect_soft_definitions($(workspace.module_name), $modules) end) end @@ -538,7 +515,7 @@ function macroexpand_in_workspace(session_notebook::SN, macrocall, cell_id, modu workspace = get_workspace(session_notebook) module_name = module_name === Symbol("") ? workspace.module_name : module_name - Distributed.remotecall_eval(Main, workspace.pid, quote + Malt.remote_eval_fetch(workspace.worker, quote try (true, PlutoRunner.try_macroexpand($(module_name), $(workspace.notebook_id), $(cell_id), $(macrocall |> QuoteNode); capture_stdout=$(capture_stdout))) catch error @@ -558,7 +535,7 @@ end function eval_fetch_in_workspace(session_notebook::Union{SN,Workspace}, expr) workspace = get_workspace(session_notebook) - Distributed.remotecall_eval(Main, workspace.pid, quote + Malt.remote_eval_fetch(workspace.worker, quote Core.eval($(workspace.module_name), $(QuoteNode(expr))) end) end @@ -566,14 +543,14 @@ end function do_reimports(session_notebook::Union{SN,Workspace}, module_imports_to_move::Set{Expr}) workspace = get_workspace(session_notebook) - Distributed.remotecall_eval(Main, [workspace.pid], quote + Malt.remote_eval_wait(workspace.worker, quote PlutoRunner.do_reimports($(workspace.module_name), $module_imports_to_move) end) end """ -Move variables to a new module. Variables to be 'deleted' will not be moved to -the new module, making them unavailable. +Move variables to a new module. A given set of variables to be 'deleted' will +not be moved to the new module, making them unavailable. """ function move_vars( session_notebook::Union{SN,Workspace}, @@ -590,7 +567,7 @@ function move_vars( workspace = get_workspace(session_notebook) new_workspace_name = something(new_workspace_name, workspace.module_name) - Distributed.remotecall_eval(Main, [workspace.pid], quote + Malt.remote_eval_wait(workspace.worker, quote PlutoRunner.move_vars( $(QuoteNode(old_workspace_name)), $(QuoteNode(new_workspace_name)), @@ -679,11 +656,6 @@ function interrupt_workspace(session_notebook::Union{SN,Workspace}; verbose=true return false end - if workspace.pid == Distributed.myid() - verbose && @warn """Cells in this workspace can't be stopped, because it is not running in a separate workspace. Use `ENV["PLUTO_WORKSPACE_USE_DISTRIBUTED"]` to control whether future workspaces are generated in a separate process.""" - return false - end - if isready(workspace.dowork_token) verbose && @info "Tried to stop idle workspace - ignoring." return true @@ -694,8 +666,8 @@ function interrupt_workspace(session_notebook::Union{SN,Workspace}; verbose=true # TODO: this will also kill "pending" evaluations, and any evaluations started within 100ms of the kill. A global "evaluation count" would fix this. # TODO: listen for the final words of the remote process on stdout/stderr: "Force throwing a SIGINT" try - verbose && @info "Sending interrupt to process $(workspace.pid)" - Distributed.interrupt(workspace.pid) + verbose && @info "Sending interrupt to process $(workspace.worker)" + Malt.interrupt(workspace.worker) if poll(() -> isready(workspace.dowork_token), 5.0, 5/100) verbose && println("Cell interrupted!") @@ -706,7 +678,7 @@ function interrupt_workspace(session_notebook::Union{SN,Workspace}; verbose=true while !isready(workspace.dowork_token) for _ in 1:5 verbose && print(" 🔥 ") - Distributed.interrupt(workspace.pid) + Malt.interrupt(workspace.worker) sleep(0.18) if isready(workspace.dowork_token) break diff --git a/src/runner/PlutoRunner.jl b/src/runner/PlutoRunner.jl index bb496c4d7e..d51a29cd85 100644 --- a/src/runner/PlutoRunner.jl +++ b/src/runner/PlutoRunner.jl @@ -1,17 +1,21 @@ # Will be evaluated _inside_ the workspace process. -# Pluto does most things on process 1 (the server), and it uses little workspace processes to evaluate notebook code in. -# These baby processes don't import Pluto, they only import this module. Functions from this module are called by WorkspaceManager.jl via Distributed +# Pluto does most things on the server, but it uses worker processes to evaluate notebook code in. +# These processes don't import Pluto, they only import this module. +# Functions from this module are called by WorkspaceManager.jl via Malt. -# So when reading this file, pretend that you are living in process 2, and you are communicating with Pluto's server, who lives in process 1. +# When reading this file, pretend that you are living in a worker process, +# and you are communicating with Pluto's server, who lives in the main process. # The package environment that this file is loaded with is the NotebookProcessProject.toml file in this directory. # SOME EXTRA NOTES # 1. The entire PlutoRunner should be a single file. -# 2. We restrict the communication between this PlutoRunner and the Pluto server to only use *Base Julia types*, like `String`, `Dict`, `NamedTuple`, etc. +# 2. Restrict the communication between this PlutoRunner and the Pluto server to only use *Base Julia types*, like `String`, `Dict`, `NamedTuple`, etc. -# These restriction are there to allow flexibility in the way that this file is loaded on a runner process, which is something that we might want to change in the future, like when we make the transition to our own Distributed. +# These restriction are there to allow flexibility in the way that this file is +# loaded on a runner process, which is something that we might want to change +# in the future. module PlutoRunner @@ -21,7 +25,6 @@ import InteractiveUtils using Markdown import Markdown: html, htmlinline, LaTeX, withtag, htmlesc -import Distributed import Base64 import FuzzyCompletions: Completion, BslashCompletion, ModuleCompletion, PropertyCompletion, FieldCompletion, PathCompletion, DictCompletion, completions, completion_text, score import Base: show, istextmime @@ -32,7 +35,7 @@ import REPL export @bind -# This is not a struct to make it easier to pass these objects between distributed processes. +# This is not a struct to make it easier to pass these objects between processes. const MimedOutput = Tuple{Union{String,Vector{UInt8},Dict{Symbol,Any}},MIME} const ObjectID = typeof(objectid("hello computer")) @@ -878,7 +881,7 @@ const table_column_display_limit_increase = 30 const tree_display_extra_items = Dict{UUID,Dict{ObjectDimPair,Int64}}() -# This is not a struct to make it easier to pass these objects between distributed processes. +# This is not a struct to make it easier to pass these objects between processes. const FormattedCellResult = NamedTuple{(:output_formatted, :errored, :interrupted, :process_exited, :runtime, :published_objects, :has_pluto_hook_features),Tuple{PlutoRunner.MimedOutput,Bool,Bool,Bool,Union{UInt64,Nothing},Dict{String,Any},Bool}} function formatted_result_of( @@ -2192,15 +2195,15 @@ function possible_bond_values(s::Symbol; get_length::Bool=false) try length(possible_values) catch - length(make_distributed_serializable(possible_values)) + length(make_serializable(possible_values)) end : - make_distributed_serializable(possible_values) + make_serializable(possible_values) end end -make_distributed_serializable(x::Any) = x -make_distributed_serializable(x::Union{AbstractVector,AbstractSet,Base.Generator}) = collect(x) -make_distributed_serializable(x::Union{Vector,Set,OrdinalRange}) = x +make_serializable(x::Any) = x +make_serializable(x::Union{AbstractVector,AbstractSet,Base.Generator}) = collect(x) +make_serializable(x::Union{Vector,Set,OrdinalRange}) = x """ diff --git a/src/webserver/REPLTools.jl b/src/webserver/REPLTools.jl index 74f432b19d..7da202f6ca 100644 --- a/src/webserver/REPLTools.jl +++ b/src/webserver/REPLTools.jl @@ -1,5 +1,5 @@ import FuzzyCompletions: complete_path, completion_text, score -import Distributed +import Malt import .PkgCompat: package_completions using Markdown import REPL @@ -83,10 +83,13 @@ responses[:complete] = function response_complete(🙋::ClientRequest) if will_run_code(🙋.notebook) && workspace isa WorkspaceManager.Workspace && isready(workspace.dowork_token) # we don't use eval_format_fetch_in_workspace because we don't want the output to be string-formatted. # This works in this particular case, because the return object, a `Completion`, exists in this scope too. - Distributed.remotecall_eval(Main, workspace.pid, :(PlutoRunner.completion_fetcher( - $query, $pos, - getfield(Main, $(QuoteNode(workspace.module_name))), - ))) + Malt.remote_eval_fetch(workspace.worker, quote + PlutoRunner.completion_fetcher( + $query, + $pos, + getfield(Main, $(QuoteNode(workspace.module_name))), + ) + end) else # We can at least autocomplete general julia things: PlutoRunner.completion_fetcher(query, pos, Main) @@ -132,10 +135,12 @@ responses[:docs] = function response_docs(🙋::ClientRequest) (repr(MIME("text/html"), doc_md), :👍) else if will_run_code(🙋.notebook) && workspace isa WorkspaceManager.Workspace && isready(workspace.dowork_token) - Distributed.remotecall_eval(Main, workspace.pid, :(PlutoRunner.doc_fetcher( - $query, - getfield(Main, $(QuoteNode(workspace.module_name))), - ))) + Malt.remote_eval_fetch(workspace.worker, quote + PlutoRunner.doc_fetcher( + $query, + getfield(Main, $(QuoteNode(workspace.module_name))), + ) + end) else (nothing, :⌛) end diff --git a/test/Bonds.jl b/test/Bonds.jl index 3fe9e559c4..bf1b028e09 100644 --- a/test/Bonds.jl +++ b/test/Bonds.jl @@ -1,7 +1,7 @@ using Test import Pluto import Pluto: update_run!, update_save_run!, WorkspaceManager, ClientSession, ServerSession, Notebook, Cell -import Distributed +import Malt @testset "Bonds" begin @@ -358,9 +358,8 @@ import Distributed # test that the notebook file is runnable: - test_proc = Distributed.addprocs(1)[1] - - Distributed.remotecall_eval(Main, test_proc, quote + test_proc = Malt.Worker() + Malt.remote_eval_wait(test_proc, quote import Pkg try Pkg.UPDATED_REGISTRY_THIS_SESSION[] = true @@ -368,11 +367,11 @@ import Distributed Pkg.activate(mktempdir()) Pkg.add("AbstractPlutoDingetjes") end) - @test Distributed.remotecall_eval(Main, test_proc, quote + @test Malt.remote_eval_fetch(test_proc, quote include($(notebook.path)) true end) - Distributed.rmprocs(test_proc) + Malt.stop(test_proc) end @testset "Dependent Bound Variables" begin diff --git a/test/Dynamic.jl b/test/Dynamic.jl index 56496f0d2d..1d8675c740 100644 --- a/test/Dynamic.jl +++ b/test/Dynamic.jl @@ -193,7 +193,7 @@ end @testset "PlutoRunner API" begin 🍭 = ServerSession() - 🍭.options.evaluation.workspace_use_distributed = true + # 🍭.options.evaluation.workspace_use_distributed = true cid = uuid1() diff --git a/test/React.jl b/test/React.jl index 0921a4ce0d..bb249032c9 100644 --- a/test/React.jl +++ b/test/React.jl @@ -1,14 +1,15 @@ using Test import Pluto: Configuration, Notebook, ServerSession, ClientSession, update_run!, Cell, WorkspaceManager import Pluto.Configuration: Options, EvaluationOptions -import Distributed @testset "Reactivity" begin 🍭 = ServerSession() 🍭.options.evaluation.workspace_use_distributed = false - @testset "Basic $(parallel ? "distributed" : "single-process")" for parallel in [false, true] - 🍭.options.evaluation.workspace_use_distributed = parallel + @testset "Basic $workertype" for workertype in [:Malt, :Distributed, :InProcess] + 🍭.options.evaluation.workspace_use_distributed = workertype !== :InProcess + 🍭.options.evaluation.workspace_use_distributed_stdlib = workertype === :Distributed + notebook = Notebook([ Cell("x = 1"), @@ -22,7 +23,13 @@ import Distributed end"""), Cell("g(6) + g(6,6)"), - Cell("import Distributed"), + Cell(""" + begin + pushfirst!(LOAD_PATH, "@stdlib") + import Distributed + popfirst!(LOAD_PATH) + end + """), Cell("Distributed.myid()"), ]) @@ -70,10 +77,14 @@ import Distributed @test notebook.cells[6].output.body == "3" update_run!(🍭, notebook, notebook.cells[7:8]) - @test if parallel - notebook.cells[8].output.body != string(Distributed.myid()) + if workertype === :Distributed + @test notebook.cells[8].output.body ∉ ("1", string(Distributed.myid())) + elseif workertype === :Malt + @test notebook.cells[8].output.body == "1" + elseif workertype === :InProcess + @test notebook.cells[8].output.body == string(Distributed.myid()) else - notebook.cells[8].output.body == string(Distributed.myid()) + error() end WorkspaceManager.unmake_workspace((🍭, notebook); verbose=false) diff --git a/test/ReloadFromFile.jl b/test/ReloadFromFile.jl index 7cc6b9aac4..ec602fee3c 100644 --- a/test/ReloadFromFile.jl +++ b/test/ReloadFromFile.jl @@ -1,7 +1,6 @@ using Test import Pluto: Configuration, Notebook, ServerSession, ClientSession, update_run!, Cell, WorkspaceManager, SessionActions, save_notebook import Pluto.Configuration: Options, EvaluationOptions -import Distributed using Pluto.WorkspaceManager: poll import Pkg diff --git a/test/WorkspaceManager.jl b/test/WorkspaceManager.jl index 92975be90a..fc7138e4a9 100644 --- a/test/WorkspaceManager.jl +++ b/test/WorkspaceManager.jl @@ -2,7 +2,7 @@ using Test using Pluto.Configuration: CompilerOptions using Pluto.WorkspaceManager: _merge_notebook_compiler_options import Pluto: update_save_run!, update_run!, WorkspaceManager, ClientSession, ServerSession, Notebook, Cell, project_relative_path -import Distributed +import Malt @testset "Workspace manager" begin # basic functionality is already tested by the reactivity tests @@ -54,7 +54,8 @@ import Distributed Sys.iswindows() || @testset "Pluto inside Pluto" begin 🍭 = ServerSession() - 🍭.options.evaluation.workspace_use_distributed = true + 🍭.options.evaluation.capture_stdout = false + 🍭.options.evaluation.workspace_use_distributed_stdlib = false notebook = Notebook([ Cell("""begin @@ -65,7 +66,10 @@ import Distributed import Pluto end"""), Cell(""" - s = Pluto.ServerSession() + begin + s = Pluto.ServerSession() + s.options.evaluation.workspace_use_distributed_stdlib = false + end """), Cell(""" nb = Pluto.SessionActions.open(s, Pluto.project_relative_path("sample", "Tower of Hanoi.jl"); run_async=false, as_sample=true) @@ -86,17 +90,10 @@ import Distributed update_run!(🍭, notebook, notebook.cells[5]) @test notebook.cells[5] |> noerror - - desired_nprocs = Distributed.nprocs() - 1 setcode!(notebook.cells[5], "Pluto.SessionActions.shutdown(s, nb)") update_run!(🍭, notebook, notebook.cells[5]) @test noerror(notebook.cells[5]) - while Distributed.nprocs() != desired_nprocs - sleep(.1) - end - sleep(.1) - WorkspaceManager.unmake_workspace((🍭, notebook)) end end diff --git a/test/cell_disabling.jl b/test/cell_disabling.jl index f5f9575dd3..3c8e8f498b 100644 --- a/test/cell_disabling.jl +++ b/test/cell_disabling.jl @@ -1,6 +1,6 @@ using Test using Pluto -using Pluto: update_run!, ServerSession, ClientSession, Cell, Notebook, set_disabled, is_disabled +using Pluto: update_run!, ServerSession, ClientSession, Cell, Notebook, set_disabled, is_disabled, WorkspaceManager @@ -236,6 +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)) end @@ -342,4 +343,5 @@ end update_run!(🍭, notebook, notebook.cells) @test get_disabled_cells(notebook) == [] + WorkspaceManager.unmake_workspace((🍭, notebook)) end diff --git a/test/helpers.jl b/test/helpers.jl index c61217a08b..3b745c25fc 100644 --- a/test/helpers.jl +++ b/test/helpers.jl @@ -16,8 +16,9 @@ import Pluto.ExpressionExplorer: SymbolsState, compute_symbolreferences, Functio using Sockets using Test using HTTP -import Distributed import Pkg +import Malt +import Malt.Distributed function Base.show(io::IO, s::SymbolsState) print(io, "SymbolsState([") @@ -243,8 +244,8 @@ has_embedded_pkgfiles(nb::Pluto.Notebook) = Log an error message if there are any running processes created by Distrubted, that were not shut down. """ function verify_no_running_processes() - if length(Distributed.procs()) != 1 - @error "Not all notebook processes were closed during tests!" Distributed.procs() + if length(Distributed.procs()) != 1 || !isempty(Malt.__iNtErNaL_get_running_procs()) + @error "Not all notebook processes were closed during tests!" Distributed.procs() Malt.__iNtErNaL_get_running_procs() end end diff --git a/test/packages/Basic.jl b/test/packages/Basic.jl index bc1b204a0b..a255865c77 100644 --- a/test/packages/Basic.jl +++ b/test/packages/Basic.jl @@ -5,7 +5,7 @@ using Pluto.Configuration: CompilerOptions import Pluto: update_save_run!, update_run!, WorkspaceManager, ClientSession, ServerSession, Notebook, Cell, project_relative_path, SessionActions, load_notebook import Pluto.PkgUtils import Pluto.PkgCompat -import Distributed +import Malt @testset "Built-in Pkg" begin @@ -13,8 +13,9 @@ import Distributed # We have our own registry for these test! Take a look at https://github.com/JuliaPluto/PlutoPkgTestRegistry#readme for more info about the test packages and their dependencies. Pkg.Registry.add(pluto_test_registry_spec) - @testset "Basic" begin + @testset "Basic $(use_distributed_stdlib ? "Distributed" : "Malt")" for use_distributed_stdlib in (false, true) 🍭 = ServerSession() + 🍭.options.evaluation.workspace_use_distributed_stdlib = use_distributed_stdlib # See https://github.com/JuliaPluto/PlutoPkgTestRegistry @@ -395,8 +396,7 @@ import Distributed end @testset "DrWatson cell" begin - 🍭 = ServerSession() - 🍭.options.evaluation.workspace_use_distributed = false + 🍭 = ServerSession() notebook = Notebook([ Cell("using Plots"), @@ -449,11 +449,11 @@ import Distributed @static if VERSION < v"1.10.0-0" # see https://github.com/fonsp/Pluto.jl/pull/2626#issuecomment-1671244510 @testset "File format -- Forwards compat" begin # Using Distributed, we will create a new Julia process in which we install Pluto 0.14.7 (before PlutoPkg). We run the new notebook file on the old Pluto. - p = Distributed.addprocs(1) |> first + test_worker = Malt.Worker() @test post_pkg_notebook isa String - Distributed.remotecall_eval(Main, p, quote + Malt.remote_eval_wait(Main, test_worker, quote path = tempname() write(path, $(post_pkg_notebook)) import Pkg @@ -462,35 +462,38 @@ import Distributed Pkg.UPDATED_REGISTRY_THIS_SESSION[] = true end - Pkg.activate(mktempdir()) + Pkg.activate(;temp=true) Pkg.add(Pkg.PackageSpec(;name="Pluto",version=v"0.14.7")) + # Distributed is required for old Pluto to work! + Pkg.add("Distributed") + import Pluto + @info Pluto.PLUTO_VERSION @assert Pluto.PLUTO_VERSION == v"0.14.7" + end) + @test Malt.remote_eval_fetch(Main, test_worker, quote s = Pluto.ServerSession() - s.options.evaluation.workspace_use_distributed = false - nb = Pluto.SessionActions.open(s, path; run_async=false) - - nothing + nb.cells[2].errored == false end) # Cells that use Example will error because the package is not installed. - # @test Distributed.remotecall_eval(Main, p, quote + # @test Malt.remote_eval_fetch(Main, test_worker, quote # nb.cells[1].errored == false # end) - @test Distributed.remotecall_eval(Main, p, quote + @test Malt.remote_eval_fetch(Main, test_worker, quote nb.cells[2].errored == false end) - # @test Distributed.remotecall_eval(Main, p, quote + # @test Malt.remote_eval_fetch(Main, test_worker, quote # nb.cells[3].errored == false # end) - # @test Distributed.remotecall_eval(Main, p, quote + # @test Malt.remote_eval_fetch(Main, test_worker, quote # nb.cells[3].output.body == "25" # end) - Distributed.rmprocs([p]) + Malt.stop(test_worker) end end @@ -768,4 +771,3 @@ end # LibGit2.checkout!(repo, "aef26d37e1d0e8f8387c011ccb7c4a38398a18f6") - diff --git a/test/runtests.jl b/test/runtests.jl index 3b3ce21a51..88901d4ae3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -10,7 +10,7 @@ if get(ENV, "PLUTO_TEST_ONLY_COMPILETIMES", nothing) == "true" end @timeit_include("Events.jl") verify_no_running_processes() -@timeit_include("WorkspaceManager.jl") +@timeit_include("Configuration.jl") verify_no_running_processes() @timeit_include("packages/Basic.jl") verify_no_running_processes() @@ -30,7 +30,7 @@ verify_no_running_processes() verify_no_running_processes() @timeit_include("Notebook.jl") verify_no_running_processes() -@timeit_include("Configuration.jl") +@timeit_include("WorkspaceManager.jl") verify_no_running_processes() # tests that don't start new processes: From 62f7898323140fda199ada9657a810f72da2513a Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Mon, 18 Sep 2023 16:04:29 +0200 Subject: [PATCH 50/52] =?UTF-8?q?=F0=9F=A6=A7=20Fix=20#2548?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Paul Berg <9824244+Pangoraw@users.noreply.github.com> --- frontend/components/CellInput.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/frontend/components/CellInput.js b/frontend/components/CellInput.js index 33cdb5e2e8..247775d413 100644 --- a/frontend/components/CellInput.js +++ b/frontend/components/CellInput.js @@ -644,14 +644,16 @@ export const CellInput = ({ if (!caused_by_window_blur) { // then it's caused by focusing something other than this cell in the editor. // in this case, we want to collapse the selection into a single point, for aesthetic reasons. - view.dispatch({ - selection: { - anchor: view.state.selection.main.head, - }, - scrollIntoView: false, - }) - // and blur the DOM again (because the previous transaction might have re-focused it) - view.contentDOM.blur() + setTimeout(() => { + view.dispatch({ + selection: { + anchor: view.state.selection.main.head, + }, + scrollIntoView: false, + }) + // and blur the DOM again (because the previous transaction might have re-focused it) + view.contentDOM.blur() + }, 0) set_cm_forced_focus(null) } From cc706f5cbe50bec9111aa65b8aca01fd9e2efd86 Mon Sep 17 00:00:00 2001 From: JL <142803222+JuliaLoetfering@users.noreply.github.com> Date: Mon, 18 Sep 2023 17:08:07 +0200 Subject: [PATCH 51/52] Added keyboard shortcut display in tooltips (#2638) Co-authored-by: Fons van der Plas --- frontend/components/Cell.js | 4 ++-- frontend/components/RunArea.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/components/Cell.js b/frontend/components/Cell.js index b43b843ae2..8ae055ab7f 100644 --- a/frontend/components/Cell.js +++ b/frontend/components/Cell.js @@ -301,7 +301,7 @@ export const Cell = ({ pluto_actions.add_remote_cell(cell_id, "before") }} class="add_cell before" - title="Add cell" + title="Add cell (Ctrl + Enter)" > @@ -359,7 +359,7 @@ export const Cell = ({ pluto_actions.add_remote_cell(cell_id, "after") }} class="add_cell after" - title="Add cell" + title="Add cell (Ctrl + Enter)" > diff --git a/frontend/components/RunArea.js b/frontend/components/RunArea.js index 6cdb33d1a6..db4b3a5fc3 100644 --- a/frontend/components/RunArea.js +++ b/frontend/components/RunArea.js @@ -33,10 +33,10 @@ export const RunArea = ({ } const titlemap = { - interrupt: "Interrupt", + interrupt: "Interrupt (Ctrl + Q)", save: "Save code without running", jump: "This cell depends on a disabled cell", - run: "Run cell", + run: "Run cell (Shift + Enter)", } const on_double_click = (/** @type {MouseEvent} */ e) => { From a7908ddecf7c40930a3ce41e71ab3312b2ab7606 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Mon, 18 Sep 2023 18:45:30 +0200 Subject: [PATCH 52/52] Malt tweaks --- src/evaluation/WorkspaceManager.jl | 20 +++++--------------- src/runner/PlutoRunner.jl | 3 +++ 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/evaluation/WorkspaceManager.jl b/src/evaluation/WorkspaceManager.jl index 0d952a7d55..ba798bb114 100644 --- a/src/evaluation/WorkspaceManager.jl +++ b/src/evaluation/WorkspaceManager.jl @@ -352,7 +352,7 @@ function unmake_workspace(session_notebook::SN; async::Bool=false, verbose::Bool nothing end -function workspace_exception_result(ex::Base.IOError, workspace::Workspace) +function workspace_exception_result(ex::Union{Base.IOError, Malt.TerminatedWorkerException, Distributed.ProcessExitedException}, workspace::Workspace) ( output_formatted=PlutoRunner.format_output(CapturedException(ex, [])), errored=true, @@ -364,9 +364,9 @@ function workspace_exception_result(ex::Base.IOError, workspace::Workspace) ) end -function workspace_exception_result(exs::CompositeException, workspace::Workspace) - ex = first(exs.exceptions) +workspace_exception_result(exs::CompositeException, workspace::Workspace) = workspace_exception_result(first(exs.exceptions), workspace) +function workspace_exception_result(ex::Exception, workspace::Workspace) if ex isa InterruptException || (ex isa Malt.RemoteException && occursin("InterruptException", ex.message)) @info "Found an interrupt!" ex ( @@ -378,23 +378,13 @@ function workspace_exception_result(exs::CompositeException, workspace::Workspac published_objects=Dict{String,Any}(), has_pluto_hook_features=false, ) - elseif ex isa Malt.TerminatedWorkerException - ( - output_formatted=PlutoRunner.format_output(CapturedException(exs, [])), - errored=true, - interrupted=true, - process_exited=true && !workspace.discarded, # don't report a process exit if the workspace was discarded on purpose - runtime=nothing, - published_objects=Dict{String,Any}(), - has_pluto_hook_features=false, - ) else @error "Unkown error during eval_format_fetch_in_workspace" ex ( - output_formatted=PlutoRunner.format_output(CapturedException(exs, [])), + output_formatted=PlutoRunner.format_output(CapturedException(ex, [])), errored=true, interrupted=true, - process_exited=false, + process_exited=!Malt.isrunning(workspace.worker) && !workspace.discarded, # don't report a process exit if the workspace was discarded on purpose runtime=nothing, published_objects=Dict{String,Any}(), has_pluto_hook_features=false, diff --git a/src/runner/PlutoRunner.jl b/src/runner/PlutoRunner.jl index d51a29cd85..cf252733ab 100644 --- a/src/runner/PlutoRunner.jl +++ b/src/runner/PlutoRunner.jl @@ -2645,6 +2645,9 @@ function with_io_to_logs(f::Function; enabled::Bool=true, loglevel::Logging.LogL _send_stdio_output!(output, loglevel) catch err @error "Failed to redirect stdout/stderr to logs" exception=(err,catch_backtrace()) + if err isa InterruptException + rethrow(err) + end end end