diff --git a/src/analysis/CellDependencyVisualization.jl b/src/analysis/CellDependencyVisualization.jl new file mode 100644 index 0000000000..98f4465c2e --- /dev/null +++ b/src/analysis/CellDependencyVisualization.jl @@ -0,0 +1,28 @@ +""" +Gets the cell number in execution order (as saved in the notebook.jl file) +""" +get_cell_number(uuid:: UUID, notebook:: Notebook):: Int = findfirst(==(uuid),notebook.cell_order) +get_cell_number(cell:: Cell, notebook:: Notebook):: Int = get_cell_number(cell.cell_id, notebook) + +""" +Gets a list of all cells on which the current cell depends on. +Changes in these cells cause re-evaluation of the current cell. +""" +get_referenced_cells(cell:: Cell, notebook:: Notebook):: Vector{Cell} = Pluto.where_referenced(notebook, notebook.topology, cell) +get_referenced_cells(uuid:: UUID, notebook:: Notebook):: Vector{Cell} = get_referenced_cells(notebook.cells_dict[uuid], notebook) + +""" +Gets a list of all cells which are dependent on the current cell. +Changes in the current cell cause re-evaluation of these cells. +""" +function get_dependent_cells(cell:: Cell, notebook:: Notebook):: Vector{Cell} + node = notebook.topology.nodes[cell] + return Pluto.where_assigned(notebook, notebook.topology, node.references) +end +get_dependent_cells(uuid:: UUID, notebook:: Notebook):: Vector{Cell} = get_dependent_cells(notebook.cells_dict[uuid], notebook) + +"Converts a list of cells to a list of UUIDs." +get_cell_uuids(cells:: Vector{Cell}):: Vector{UUID} = getproperty.(cells, :cell_id) + +"Converts a list of cells to a list of execution order cell numbers." +get_cell_numbers(cells:: Vector{Cell}, notebook:: Notebook):: Vector{Int} = get_cell_number.(get_cell_uuids(cells), Ref(notebook)) diff --git a/src/analysis/DependencyCache.jl b/src/analysis/DependencyCache.jl index b73d630d97..848816a160 100644 --- a/src/analysis/DependencyCache.jl +++ b/src/analysis/DependencyCache.jl @@ -42,14 +42,30 @@ function update_dependency_cache!(cell::Cell, notebook::Notebook) downstream_cells_map(cell, notebook), upstream_cells_map(cell, notebook), cell_precedence_heuristic(notebook.topology, cell), + Ref(cell.running_disabled), contains_user_defined_macrocalls(cell, notebook) ) end "Fills dependency information on notebook and cell level." -function update_dependency_cache!(notebook::Notebook) +function update_dependency_cache!(notebook::Notebook, topology:: NotebookTopology) notebook._cached_topological_order = topological_order(notebook) for cell in values(notebook.cells_dict) update_dependency_cache!(cell, notebook) end + disable_dependent_cells!(notebook, topology) +end + +""" +find (indirectly) deactivated cells and update their status +""" +function disable_dependent_cells!(notebook:: Notebook, topology:: NotebookTopology):: Vector{Cell} + deactivated = filter(c -> c.running_disabled, notebook.cells) + indirectly_deactivated = collect(topological_order(notebook, topology, deactivated)) + for cell in indirectly_deactivated + cell.running = false + cell.queued = false + cell.cell_dependencies.depends_on_disabled_cells[] = true + end + return indirectly_deactivated end diff --git a/src/evaluation/Run.jl b/src/evaluation/Run.jl index 995f97c37b..4841f0e1ae 100644 --- a/src/evaluation/Run.jl +++ b/src/evaluation/Run.jl @@ -18,7 +18,7 @@ function run_reactive!(session::ServerSession, notebook::Notebook, old_topology: new_topology = notebook.topology = resolve_topology(session, notebook, unresolved_topology, old_workspace_name) # update cache and save notebook because the dependencies might have changed after expanding macros - update_dependency_cache!(notebook) + update_dependency_cache!(notebook, notebook.topology) session.options.server.disable_writing_notebook_files || save_notebook(notebook) end else @@ -51,23 +51,12 @@ function run_reactive!(session::ServerSession, notebook::Notebook, old_topology: # get the new topological order new_order = topological_order(notebook, new_topology, union(roots, keys(old_order.errable))) - to_run_raw = setdiff(union(new_order.runnable, old_order.runnable), keys(new_order.errable))::Vector{Cell} # TODO: think if old error cell order matters - - # find (indirectly) deactivated cells and update their status - deactivated = filter(c -> c.running_disabled, notebook.cells) - indirectly_deactivated = collect(topological_order(notebook, new_topology, deactivated)) - for cell in indirectly_deactivated - cell.running = false - cell.queued = false - cell.depends_on_disabled_cells = true - end - - to_run = setdiff(to_run_raw, indirectly_deactivated) + to_run_including_deactivated = setdiff(union(new_order.runnable, old_order.runnable), keys(new_order.errable))::Vector{Cell} # TODO: think if old error cell order matters + to_run = filter(c -> !c.cell_dependencies.depends_on_disabled_cells[], to_run_including_deactivated) # change the bar on the sides of cells to "queued" - for cell in to_run - cell.queued = true - cell.depends_on_disabled_cells = false + for cell in to_run_including_deactivated + cell.queued = cell.running = !cell.cell_dependencies.depends_on_disabled_cells[] end for (cell, error) in new_order.errable cell.running = false @@ -302,7 +291,7 @@ function update_save_run!(session::ServerSession, notebook::Notebook, cells::Arr old = notebook.topology new = notebook.topology = updated_topology(old, notebook, cells) # macros are not yet resolved - update_dependency_cache!(notebook) + update_dependency_cache!(notebook, new) session.options.server.disable_writing_notebook_files || (save && save_notebook(notebook)) # _assume `prerender_text == false` if you want to skip some details_ diff --git a/src/notebook/Cell.jl b/src/notebook/Cell.jl index 6e881342fe..c345543738 100644 --- a/src/notebook/Cell.jl +++ b/src/notebook/Cell.jl @@ -17,6 +17,7 @@ struct CellDependencies{T} # T == Cell, but this has to be parametric to avoid a downstream_cells_map::Dict{Symbol,Vector{T}} upstream_cells_map::Dict{Symbol,Vector{T}} precedence_heuristic::Int + depends_on_disabled_cells:: Base.RefValue{Bool} # mutable value in immutable struct contains_user_defined_macrocalls::Bool end @@ -38,10 +39,9 @@ Base.@kwdef mutable struct Cell runtime::Union{Nothing,UInt64}=nothing # note that this field might be moved somewhere else later. If you are interested in visualizing the cell dependencies, take a look at the cell_dependencies field in the frontend instead. - cell_dependencies::CellDependencies{Cell}=CellDependencies{Cell}(Dict{Symbol,Vector{Cell}}(), Dict{Symbol,Vector{Cell}}(), 99, false) + cell_dependencies::CellDependencies{Cell}=CellDependencies{Cell}(Dict{Symbol,Vector{Cell}}(), Dict{Symbol,Vector{Cell}}(), 99, Ref(false), false) running_disabled::Bool=false - depends_on_disabled_cells::Bool=false end Cell(cell_id, code) = Cell(cell_id=cell_id, code=code) diff --git a/src/notebook/Notebook.jl b/src/notebook/Notebook.jl index 258bd7008a..2067019e79 100644 --- a/src/notebook/Notebook.jl +++ b/src/notebook/Notebook.jl @@ -84,11 +84,15 @@ end const _notebook_header = "### A Pluto.jl notebook ###" # We use a creative delimiter to avoid accidental use in code # so don't get inspired to suddenly use these in your code! -const _cell_id_delimiter = "# ╔═╡ " -const _order_delimiter = "# ╠═" -const _order_delimiter_folded = "# ╟─" +const _cell_id_delimiter = "# ╔═╡ " +const _order_delimiter = "# ╠═" +const _order_delimiter_folded = "# ╟─" const _cell_suffix = "\n\n" +const _running_disabled_prefix = "#=╠═╡ disabled\n" +const _running_disabled_suffix = "\n ╠═╡ disabled =#" +const _depends_on_disabled_cells_prefix = "#=╠═╡ depends on disabled cell(s)\n" +const _depends_on_disabled_cells_suffix = "\n ╠═╡ depends on disabled cell(s) =#" const _ptoml_cell_id = UUID(1) const _mtoml_cell_id = UUID(2) @@ -121,7 +125,21 @@ function save_notebook(io, notebook::Notebook) for c in cells_ordered println(io, _cell_id_delimiter, string(c.cell_id)) # write the cell code and prevent collisions with the cell delimiter - print(io, replace(c.code, _cell_id_delimiter => "# ")) + + if c.running_disabled + print(io, _running_disabled_prefix) + print(io, replace(c.code, _cell_id_delimiter => "# ")) + print(io, _running_disabled_suffix) + elseif c.cell_dependencies.depends_on_disabled_cells[] + # if a cell is both disabled directly and indirectly, the first has higher priority + print(io, _depends_on_disabled_cells_prefix) + print(io, replace(c.code, _cell_id_delimiter => "# ")) + print(io, _depends_on_disabled_cells_suffix) + else + # cell is not disabled on startup + print(io, replace(c.code, _cell_id_delimiter => "# ")) + end + print(io, _cell_suffix) end @@ -211,10 +229,19 @@ function load_notebook_nobackup(io, path)::Notebook code_raw = String(readuntil(io, _cell_id_delimiter)) # change Windows line endings to Linux code_normalised = replace(code_raw, "\r\n" => "\n") + + # get the information if a cell is disabled + running_disabled = startswith(code_normalised, _running_disabled_prefix) + + # remove the disabled on startup comments for further processing in Julia + code_normalised = replace(replace(code_normalised, _running_disabled_prefix => ""), _running_disabled_suffix => "") + code_normalised = replace(replace(code_normalised, _depends_on_disabled_cells_prefix => ""), _depends_on_disabled_cells_suffix => "") + # remove the cell suffix code = code_normalised[1:prevind(code_normalised, end, length(_cell_suffix))] read_cell = Cell(cell_id, code) + read_cell.running_disabled = running_disabled collected_cells[cell_id] = read_cell end end @@ -299,7 +326,7 @@ function load_notebook(path::String; disable_writing_notebook_files::Bool=false) loaded = load_notebook_nobackup(path) # Analyze cells so that the initial save is in topological order loaded.topology = updated_topology(loaded.topology, loaded, loaded.cells) |> static_resolve_topology - update_dependency_cache!(loaded) + update_dependency_cache!(loaded, loaded.topology) disable_writing_notebook_files || save_notebook(loaded) loaded.topology = NotebookTopology() diff --git a/src/webserver/Dynamic.jl b/src/webserver/Dynamic.jl index 7868a47d0c..dced2c2471 100644 --- a/src/webserver/Dynamic.jl +++ b/src/webserver/Dynamic.jl @@ -124,7 +124,7 @@ function notebook_to_js(notebook::Notebook) "cell_results" => Dict{UUID,Dict{String,Any}}( id => Dict{String,Any}( "cell_id" => cell.cell_id, - "depends_on_disabled_cells" => cell.depends_on_disabled_cells, + "depends_on_disabled_cells" => cell.cell_dependencies.depends_on_disabled_cells[], "output" => Dict( "body" => cell.output.body, "mime" => cell.output.mime, diff --git a/test/cell_disabling.jl b/test/cell_disabling.jl index 7e1eb83a72..676922d679 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 +using Pluto: update_run!, ServerSession, ClientSession, Cell, Notebook, save_notebook @testset "Cell Disabling" begin 🍭 = ServerSession() @@ -22,18 +22,25 @@ using Pluto: update_run!, ServerSession, ClientSession, Cell, Notebook fakeclient.connected_notebook = notebook update_run!(🍭, notebook, notebook.cells) + notebook_file = tempname() + # helper functions id(i) = notebook.cells[i].cell_id - get_disabled_cells(notebook) = [i for (i, c) in pairs(notebook.cells) if c.depends_on_disabled_cells] + get_disabled_cells(notebook) = [i for (i, c) in pairs(notebook.cells) if c.cell_dependencies.depends_on_disabled_cells[]] @test !any(c.running_disabled for c in notebook.cells) - @test !any(c.depends_on_disabled_cells for c in notebook.cells) + @test !any(c.cell_dependencies.depends_on_disabled_cells[] for c in notebook.cells) + + save_notebook(notebook, notebook_file) + @test jl_is_runnable(notebook_file) # disable first cell notebook.cells[1].running_disabled = true update_run!(🍭, notebook, notebook.cells) should_be_disabled = [1, 3, 5] @test get_disabled_cells(notebook) == should_be_disabled + save_notebook(notebook, notebook_file) + @test jl_is_runnable(notebook_file) # change x, this change should not propagate through y original_y_output = notebook.cells[1].output.body @@ -46,6 +53,8 @@ using Pluto: update_run!, ServerSession, ClientSession, Cell, Notebook @test notebook.cells[3].output.body == original_z_output @test notebook.cells[4].output.body != original_a_output @test notebook.cells[5].output.body == original_w_output + save_notebook(notebook, notebook_file) + @test jl_is_runnable(notebook_file) setcode(notebook.cells[2], "x = 2") update_run!(🍭, notebook, notebook.cells[2]) @@ -53,24 +62,31 @@ using Pluto: update_run!, ServerSession, ClientSession, Cell, Notebook @test notebook.cells[3].output.body == original_z_output @test notebook.cells[4].output.body == original_a_output @test notebook.cells[5].output.body == original_w_output + save_notebook(notebook, notebook_file) + @test jl_is_runnable(notebook_file) # disable root cell notebook.cells[2].running_disabled = true update_run!(🍭, notebook, notebook.cells) @test get_disabled_cells(notebook) == collect(1:5) - + save_notebook(notebook, notebook_file) + @test jl_is_runnable(notebook_file) original_6_output = notebook.cells[6].output.body setcode(notebook.cells[6], "x + 6") update_run!(🍭, notebook, notebook.cells[6]) - @test notebook.cells[6].depends_on_disabled_cells + @test notebook.cells[6].cell_dependencies.depends_on_disabled_cells[] @test notebook.cells[6].errored === false @test notebook.cells[6].output.body == original_6_output + save_notebook(notebook, notebook_file) + @test jl_is_runnable(notebook_file) # reactivate first cell - still all cells should be running_disabled notebook.cells[1].running_disabled = false update_run!(🍭, notebook, notebook.cells) @test get_disabled_cells(notebook) == collect(1:6) + save_notebook(notebook, notebook_file) + @test jl_is_runnable(notebook_file) # the x cell is disabled, so changing it should have no effect setcode(notebook.cells[2], "x = 123123") @@ -79,27 +95,34 @@ using Pluto: update_run!, ServerSession, ClientSession, Cell, Notebook @test notebook.cells[3].output.body == original_z_output @test notebook.cells[4].output.body == original_a_output @test notebook.cells[5].output.body == original_w_output + save_notebook(notebook, notebook_file) + @test jl_is_runnable(notebook_file) # reactivate root cell notebook.cells[2].running_disabled = false update_run!(🍭, notebook, notebook.cells) @test get_disabled_cells(notebook) == [] - @test notebook.cells[1].output.body != original_y_output @test notebook.cells[3].output.body != original_z_output @test notebook.cells[4].output.body != original_a_output @test notebook.cells[5].output.body != original_w_output + save_notebook(notebook, notebook_file) + @test jl_is_runnable(notebook_file) # disable first cell again notebook.cells[1].running_disabled = true update_run!(🍭, notebook, notebook.cells) should_be_disabled = [1, 3, 5] @test get_disabled_cells(notebook) == should_be_disabled + save_notebook(notebook, notebook_file) + @test jl_is_runnable(notebook_file) # and reactivate it notebook.cells[1].running_disabled = false update_run!(🍭, notebook, notebook.cells) @test get_disabled_cells(notebook) == [] + save_notebook(notebook, notebook_file) + @test jl_is_runnable(notebook_file) end