Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions frontend/components/CellOutput.js
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,11 @@ const execute_scripttags = async ({ root_node, script_nodes, previous_results_ma
// @ts-ignore
getPublishedObject: (id) => cell.getPublishedObject(id),

_internal_getJSLinkResponse: (cell_id, link_id) => (input) =>
pluto_actions.request_js_link_response(cell_id, link_id, input).then(([success, result]) => {
if (success) return result
throw result
}),
getBoundElementValueLikePluto: get_input_value,
setBoundElementValueLikePluto: set_input_value,
getBoundElementEventNameLikePluto: eventof,
Expand Down
13 changes: 13 additions & 0 deletions frontend/components/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,19 @@ export class Editor extends Component {
false
)
},
request_js_link_response: (cell_id, link_id, input) => {
return this.client
.send(
"request_js_link_response",
{
cell_id,
link_id,
input,
},
{ notebook_id: this.state.notebook.notebook_id }
)
.then((r) => r.message)
},
/** This actions avoids pushing selected cells all the way down, which is too heavy to handle! */
get_selected_cells: (cell_id, /** @type {boolean} */ allow_other_selected_cells) =>
allow_other_selected_cells ? this.state.selected_cells : [cell_id],
Expand Down
5 changes: 3 additions & 2 deletions src/evaluation/Run.jl
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,9 @@ function run_reactive_core!(
new_errable = keys(new_order.errable)
to_delete_vars = union!(to_delete_vars, defined_variables(new_topology, new_errable)...)
to_delete_funcs = union!(to_delete_funcs, defined_functions(new_topology, new_errable)...)

cells_to_macro_invalidate = Set{UUID}(c.cell_id for c in cells_with_deleted_macros(old_topology, new_topology))
cells_to_js_link_invalidate = Set{UUID}(c.cell_id for c in union!(Set{Cell}(), to_run, new_errable, indirectly_deactivated))

module_imports_to_move = reduce(all_cells(new_topology); init=Set{Expr}()) do module_imports_to_move, c
c ∈ to_run && return module_imports_to_move
Expand All @@ -156,7 +157,7 @@ function run_reactive_core!(

if will_run_code(notebook)
to_delete_funcs_simple = Set{Tuple{UUID,Tuple{Vararg{Symbol}}}}((id, name.parts) for (id,name) in to_delete_funcs)
deletion_hook((session, notebook), old_workspace_name, nothing, to_delete_vars, to_delete_funcs_simple, module_imports_to_move, cells_to_macro_invalidate; to_run) # `deletion_hook` defaults to `WorkspaceManager.move_vars`
deletion_hook((session, notebook), old_workspace_name, nothing, to_delete_vars, to_delete_funcs_simple, module_imports_to_move, cells_to_macro_invalidate, cells_to_js_link_invalidate; to_run) # `deletion_hook` defaults to `WorkspaceManager.move_vars`
end

foreach(v -> delete!(notebook.bonds, v), to_delete_vars)
Expand Down
3 changes: 2 additions & 1 deletion src/evaluation/RunBonds.jl
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function set_bond_values_reactive(;
bond_value_pairs = zip(syms_to_set, new_values)

syms_to_set_set = Set{Symbol}(syms_to_set)
function custom_deletion_hook((session, notebook)::Tuple{ServerSession,Notebook}, old_workspace_name, new_workspace_name, to_delete_vars::Set{Symbol}, methods_to_delete, module_imports_to_move, cells_to_macro_invalidate; to_run)
function custom_deletion_hook((session, notebook)::Tuple{ServerSession,Notebook}, old_workspace_name, new_workspace_name, to_delete_vars::Set{Symbol}, methods_to_delete, module_imports_to_move, cells_to_macro_invalidate, cells_to_js_link_invalidate; to_run)
to_delete_vars = union(to_delete_vars, syms_to_set_set) # also delete the bound symbols
WorkspaceManager.move_vars(
(session, notebook),
Expand All @@ -51,6 +51,7 @@ function set_bond_values_reactive(;
methods_to_delete,
module_imports_to_move,
cells_to_macro_invalidate,
cells_to_js_link_invalidate,
syms_to_set_set,
)
set_bond_value_pairs!(session, notebook, zip(syms_to_set, new_values))
Expand Down
10 changes: 7 additions & 3 deletions src/evaluation/WorkspaceManager.jl
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,7 @@ function move_vars(
methods_to_delete::Set{Tuple{UUID,Tuple{Vararg{Symbol}}}},
module_imports_to_move::Set{Expr},
cells_to_macro_invalidate::Set{UUID},
cells_to_js_link_invalidate::Set{UUID},
keep_registered::Set{Symbol}=Set{Symbol}();
kwargs...
)
Expand All @@ -570,6 +571,7 @@ function move_vars(
$methods_to_delete,
$module_imports_to_move,
$cells_to_macro_invalidate,
$cells_to_js_link_invalidate,
$keep_registered,
)
end)
Expand All @@ -580,16 +582,18 @@ function move_vars(
to_delete::Set{Symbol},
methods_to_delete::Set{Tuple{UUID,Tuple{Vararg{Symbol}}}},
module_imports_to_move::Set{Expr},
cells_to_macro_invalidate::Set{UUID};
cells_to_macro_invalidate::Set{UUID},
cells_to_js_link_invalidate::Set{UUID};
kwargs...
)
move_vars(
session_notebook,
bump_workspace_module(session_notebook)...,
to_delete,
to_delete,
methods_to_delete,
module_imports_to_move,
cells_to_macro_invalidate;
cells_to_macro_invalidate,
cells_to_js_link_invalidate;
kwargs...
)
end
Expand Down
107 changes: 91 additions & 16 deletions src/runner/PlutoRunner/src/PlutoRunner.jl
Original file line number Diff line number Diff line change
Expand Up @@ -283,14 +283,9 @@ function try_macroexpand(mod::Module, notebook_id::UUID, cell_id::UUID, expr; ca
Expr(:block, expr)
end

logger = get!(() -> PlutoCellLogger(notebook_id, cell_id), pluto_cell_loggers, cell_id)
if logger.workspace_count < moduleworkspace_count[]
logger = pluto_cell_loggers[cell_id] = PlutoCellLogger(notebook_id, cell_id)
end
capture_logger = CaptureLogger(nothing, get_cell_logger(notebook_id, cell_id), Dict[])

capture_logger = CaptureLogger(nothing, logger, Dict[])

expanded_expr, elapsed_ns = with_logger_and_io_to_logs(capture_logger; capture_stdout, stdio_loglevel=stdout_log_level) do
expanded_expr, elapsed_ns = with_logger_and_io_to_logs(capture_logger; capture_stdout) do
elapsed_ns = time_ns()
expanded_expr = macroexpand(mod, expr_not_toplevel)::Expr
elapsed_ns = time_ns() - elapsed_ns
Expand Down Expand Up @@ -531,17 +526,17 @@ function run_expression(
old_currently_running_cell_id = currently_running_cell_id[]
currently_running_cell_id[] = cell_id

logger = get!(() -> PlutoCellLogger(notebook_id, cell_id), pluto_cell_loggers, cell_id)
if logger.workspace_count < moduleworkspace_count[]
logger = pluto_cell_loggers[cell_id] = PlutoCellLogger(notebook_id, cell_id)
end
logger = get_cell_logger(notebook_id, cell_id)

# reset published objects
cell_published_objects[cell_id] = Dict{String,Any}()

# reset registered bonds
cell_registered_bond_names[cell_id] = Set{Symbol}()

# reset JS links
unregister_js_link(cell_id)

# If the cell contains macro calls, we want those macro calls to preserve their identity,
# so we macroexpand this earlier (during expression explorer stuff), and then we find it here.
# NOTE Turns out sometimes there is no macroexpanded version even though the expression contains macro calls...
Expand Down Expand Up @@ -593,7 +588,7 @@ function run_expression(
throw("Expression still contains macro calls!!")
end

result, runtime = with_logger_and_io_to_logs(logger; capture_stdout, stdio_loglevel=stdout_log_level) do # about 200ns + 3ms overhead
result, runtime = with_logger_and_io_to_logs(logger; capture_stdout) do # about 200ns + 3ms overhead
if function_wrapped_info === nothing
toplevel_expr = Expr(:toplevel, expr)
wrapped = timed_expr(toplevel_expr)
Expand Down Expand Up @@ -691,6 +686,7 @@ function move_vars(
methods_to_delete::Set{Tuple{UUID,Tuple{Vararg{Symbol}}}},
module_imports_to_move::Set{Expr},
cells_to_macro_invalidate::Set{UUID},
cells_to_js_link_invalidate::Set{UUID},
keep_registered::Set{Symbol},
)
old_workspace = getfield(Main, old_workspace_name)
Expand All @@ -701,7 +697,8 @@ function move_vars(
for cell_id in cells_to_macro_invalidate
delete!(cell_expanded_exprs, cell_id)
end

foreach(unregister_js_link, cells_to_js_link_invalidate)

# TODO: delete
Core.eval(new_workspace, :(import ..($(old_workspace_name))))

Expand Down Expand Up @@ -929,8 +926,7 @@ function formatted_result_of(
errored = ans isa CapturedException

output_formatted = if (!ends_with_semicolon || errored)
logger = get!(() -> PlutoCellLogger(notebook_id, cell_id), pluto_cell_loggers, cell_id)
with_logger_and_io_to_logs(logger; capture_stdout, stdio_loglevel=stdout_log_level) do
with_logger_and_io_to_logs(get_cell_logger(notebook_id, cell_id); capture_stdout) do
format_output(ans; context=IOContext(
default_iocontext,
:extra_items=>extra_items,
Expand Down Expand Up @@ -1002,6 +998,7 @@ const default_iocontext = IOContext(devnull,
:is_pluto => true,
:pluto_supported_integration_features => supported_integration_features,
:pluto_published_to_js => (io, x) -> core_published_to_js(io, x),
:pluto_with_js_link => (io, callback, on_cancellation) -> core_with_js_link(io, callback, on_cancellation),
)

const default_stdout_iocontext = IOContext(devnull,
Expand Down Expand Up @@ -1757,6 +1754,9 @@ const integrations = Integration[
if isdefined(AbstractPlutoDingetjes.Display, :published_to_js)
supported!(AbstractPlutoDingetjes.Display.published_to_js)
end
if isdefined(AbstractPlutoDingetjes.Display, :with_js_link)
supported!(AbstractPlutoDingetjes.Display.with_js_link)
end
end

end,
Expand Down Expand Up @@ -2540,6 +2540,73 @@ function Base.show(io::IO, m::MIME"text/html", e::DivElement)
Base.show(io, m, embed_display(e))
end


###
# JS LINK
###

struct JSLink
callback::Function
on_cancellation::Union{Nothing,Function}
cancelled_ref::Ref{Bool}
end

const cell_js_links = Dict{UUID,Dict{String,JSLink}}()

function core_with_js_link(io, callback, on_cancellation)

_cell_id = get(io, :pluto_cell_id, currently_running_cell_id[])::UUID

link_id = String(rand('a':'z', 16))

links = get!(() -> Dict{String,JSLink}(), cell_js_links, _cell_id)
links[link_id] = JSLink(callback, on_cancellation, Ref(false))

write(io, "/* See the documentation for AbstractPlutoDingetjes.Display.with_js_link */ _internal_getJSLinkResponse(\"$(_cell_id)\", \"$(link_id)\")")
end

function unregister_js_link(cell_id::UUID)
# cancel old links
old_links = get!(() -> Dict{String,JSLink}(), cell_js_links, cell_id)
for (name, link) in old_links
link.cancelled_ref[] = true
end
for (name, link) in old_links
c = link.on_cancellation
c === nothing || c()
end

# clear
cell_js_links[cell_id] = Dict{String,JSLink}()
end

function evaluate_js_link(notebook_id::UUID, cell_id::UUID, link_id::String, input::Any)
links = get(() -> Dict{String,JSLink}(), cell_js_links, cell_id)
link = get(links, link_id, nothing)

with_logger_and_io_to_logs(get_cell_logger(notebook_id, cell_id); capture_stdout=false) do
if link === nothing
@warn "🚨 AbstractPlutoDingetjes: JS link not found." link_id

(false, "link not found")
elseif link.cancelled_ref[]
@warn "🚨 AbstractPlutoDingetjes: JS link has already been invalidated." link_id

(false, "link has been invalidated")
else
try
result = link.callback(input)
assertpackable(result)

(true, result)
catch ex
@error "🚨 AbstractPlutoDingetjes.Display.with_js_link: Exception while evaluating Julia callback." input exception=(ex, catch_backtrace())
(false, "exception in Julia callback:\n\n$(ex)")
end
end
end
end

###
# LOGGING
###
Expand Down Expand Up @@ -2581,6 +2648,14 @@ end
const pluto_cell_loggers = Dict{UUID,PlutoCellLogger}() # One logger per cell
const pluto_log_channels = Dict{UUID,Channel{Any}}() # One channel per notebook

function get_cell_logger(notebook_id, cell_id)
logger = get!(() -> PlutoCellLogger(notebook_id, cell_id), pluto_cell_loggers, cell_id)
if logger.workspace_count < moduleworkspace_count[]
logger = pluto_cell_loggers[cell_id] = PlutoCellLogger(notebook_id, cell_id)
end
logger
end

function Logging.shouldlog(logger::PlutoCellLogger, level, _module, _...)
# Accept logs
# - Only if the logger is the latest for this cell using the increasing workspace_count tied to each logger
Expand Down Expand Up @@ -2743,7 +2818,7 @@ function with_io_to_logs(f::Function; enabled::Bool=true, loglevel::Logging.LogL
result
end

function with_logger_and_io_to_logs(f, logger; capture_stdout=true, stdio_loglevel=Logging.LogLevel(1))
function with_logger_and_io_to_logs(f, logger; capture_stdout=true, stdio_loglevel=stdout_log_level)
Logging.with_logger(logger) do
with_io_to_logs(f; enabled=capture_stdout, loglevel=stdio_loglevel)
end
Expand Down
23 changes: 23 additions & 0 deletions src/webserver/Dynamic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,29 @@ responses[:reshow_cell] = function response_reshow_cell(🙋::ClientRequest)
send_notebook_changes!(🙋 |> without_initiator)
end

responses[:request_js_link_response] = function response_request_js_link_response(🙋::ClientRequest)
require_notebook(🙋)
@assert will_run_code(🙋.notebook)

Threads.@spawn try
result = WorkspaceManager.eval_fetch_in_workspace(
(🙋.session, 🙋.notebook),
quote
PlutoRunner.evaluate_js_link(
$(🙋.notebook.notebook_id),
$(UUID(🙋.body["cell_id"])),
$(🙋.body["link_id"]),
$(🙋.body["input"]),
)
end
)

putclientupdates!(🙋.session, 🙋.initiator, UpdateMessage(:🐤, result, nothing, nothing, 🙋.initiator))
catch ex
@error "Error in request_js_link_response" exception=(ex, stacktrace(catch_backtrace()))
end
end

responses[:nbpkg_available_versions] = function response_nbpkg_available_versions(🙋::ClientRequest)
# require_notebook(🙋)
all_versions = PkgCompat.package_versions(🙋.body["package_name"])
Expand Down
8 changes: 4 additions & 4 deletions test/frontend/__tests__/javascript_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,15 @@ describe("JavaScript API", () => {
page,
`# ╔═╡ 90cfa9a0-114d-49bf-8dea-e97d58fa2442
html"""<script>
const div = document.createElement("div")
const div = document.createElement("find-me")
div.innerHTML = "${expected}"
return div;
</script>"""
`
)
await runAllChanged(page)
await waitForPlutoToCalmDown(page, { polling: 100 })
const initialLastCellContent = await waitForContentToBecome(page, `pluto-cell:last-child pluto-output`, expected)
const initialLastCellContent = await waitForContentToBecome(page, `pluto-cell:last-child pluto-output find-me`, expected)
expect(initialLastCellContent).toBe(expected)
})

Expand All @@ -69,7 +69,7 @@ describe("JavaScript API", () => {
)
await runAllChanged(page)
await waitForPlutoToCalmDown(page, { polling: 100 })
let initialLastCellContent = await waitForContentToBecome(page, `pluto-cell:last-child pluto-output`, expected)
let initialLastCellContent = await waitForContentToBecome(page, `pluto-cell:last-child pluto-output span`, expected)
expect(initialLastCellContent).toBe(expected)

await paste(
Expand All @@ -84,7 +84,7 @@ describe("JavaScript API", () => {
)
await runAllChanged(page)
await waitForPlutoToCalmDown(page, { polling: 100 })
initialLastCellContent = await waitForContentToBecome(page, `pluto-cell:last-child pluto-output`, expected)
initialLastCellContent = await waitForContentToBecome(page, `pluto-cell:last-child pluto-output span`, expected)
expect(initialLastCellContent).toBe(expected)
})

Expand Down
2 changes: 1 addition & 1 deletion test/frontend/__tests__/slide_controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe("slideControls", () => {
await importNotebook(page, "slides.jl", { permissionToRunCode: false })
const plutoCellIds = await getCellIds(page)
const content = await waitForContent(page, `pluto-cell[id="${plutoCellIds[1]}"] pluto-output`)
expect(content).toBe("Slide 2")
expect(content).toBe("Slide 2\n")

const slide_1_title = await page.$(`pluto-cell[id="${plutoCellIds[0]}"] pluto-output h1`)
const slide_2_title = await page.$(`pluto-cell[id="${plutoCellIds[1]}"] pluto-output h1`)
Expand Down
4 changes: 2 additions & 2 deletions test/frontend/__tests__/wind_directions.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,14 @@ describe("wind_directions", () => {
).toBe(expected)
}

await expect_chosen_directions('chosen_directions_copy\n"North"')
await expect_chosen_directions('chosen_directions_copyString1"North"')

expect(await page.evaluate((sel) => document.querySelector(sel).checked, checkbox_selector(0))).toBe(true)

await page.click(checkbox_selector(2))
await waitForPlutoToCalmDown(page)

await expect_chosen_directions('chosen_directions_copy\n"North"\n"South"')
await expect_chosen_directions('chosen_directions_copyString1"North"2"South"')

expect(await page.evaluate((sel) => document.querySelector(sel).checked, checkbox_selector(0))).toBe(true)
expect(await page.evaluate((sel) => document.querySelector(sel).checked, checkbox_selector(1))).toBe(false)
Expand Down
Loading