forked from compleathorseplayer/Neptune.jl
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Notebook.jl
206 lines (170 loc) · 7.93 KB
/
Notebook.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
import UUIDs: UUID, uuid1
import .ExpressionExplorer: SymbolsState
struct NotebookTopology
symstates::Dict{Cell,SymbolsState}
combined_funcdefs::Dict{Vector{Symbol},SymbolsState}
end
NotebookTopology() = NotebookTopology(Dict{Cell,SymbolsState}(), Dict{Vector{Symbol},SymbolsState}())
# `topology[cell]` is a shorthand for `get(topology, cell, SymbolsState())`
# with the performance benefit of only generating SymbolsState() when needed
function Base.getindex(topology::NotebookTopology, cell::Cell)
result = get(topology.symstates, cell, nothing)
result === nothing ? SymbolsState() : result
end
mutable struct Notebook
"Cells are ordered in a `Notebook`, and this order can be changed by the user. Cells will always have a constant UUID."
cells::Array{Cell,1}
path::AbstractString
notebook_id::UUID
topology::NotebookTopology
# buffer will contain all unfetched updates - must be big enough
pendingupdates::Channel
executetoken::Token
end
# We can keep 128 updates pending. After this, any put! calls (i.e. calls that push an update to the notebook) will simply block, which is fine.
# This does mean that the Notebook can't be used if nothing is clearing the update channel.
Notebook(cells::Array{Cell,1}, path::AbstractString, notebook_id::UUID) =
Notebook(cells, path, notebook_id, NotebookTopology(), Channel(1024), Token())
Notebook(cells::Array{Cell,1}, path::AbstractString=numbered_until_new(joinpath(tempdir(), cutename()))) = Notebook(cells, path, uuid1())
function cell_index_from_id(notebook::Notebook, cell_id::UUID)::Union{Int,Nothing}
findfirst(c -> c.cell_id == cell_id, notebook.cells)
end
const _notebook_header = "### A Pluto.jl notebook ###"
# We use a creative delimiter to avoid accidental use in code
const _cell_id_delimiter = "# ╔═╡ "
const _order_delimiter = "# ╠═"
const _order_delimiter_folded = "# ╟─"
const _cell_suffix = "\n\n"
emptynotebook(args...) = Notebook([Cell()], args...)
function save_notebook(io, notebook::Notebook)
println(io, _notebook_header)
println(io, "# ", PLUTO_VERSION_STR)
# Anything between the version string and the first UUID delimiter will be ignored by the notebook loader.
println(io, "")
println(io, "using Markdown")
println(io, "using InteractiveUtils")
# Super Advanced Code Analysis™ to add the @bind macro to the saved file if it's used somewhere.
if any(occursin("@bind", c.code) for c in notebook.cells)
println(io, "")
println(io, "# This Pluto notebook uses @bind for interactivity. When running this notebook outside of Pluto, the following 'mock version' of @bind gives bound variables a default value (instead of an error).")
println(io, PlutoRunner.fake_bind)
end
println(io)
# TODO: this can be optimised by caching the topological order:
# maintain cache with ordered UUIDs
# whenever a run_reactive! is done, move the found cells **down** until they are in one group, and order them topologically within that group. Errable cells go to the bottom.
# the next call took 2ms for a small-medium sized notebook: (so not too bad)
# 15 ms for a massive notebook - 120 cells, 800 lines
notebook_topo_order = topological_order(notebook, notebook.topology, notebook.cells)
cells_ordered = union(notebook_topo_order.runnable, keys(notebook_topo_order.errable))
for c in cells_ordered
println(io, _cell_id_delimiter, string(c.cell_id))
print(io, c.code)
print(io, _cell_suffix)
end
println(io, _cell_id_delimiter, "Cell order:")
for c in notebook.cells
delim = c.code_folded ? _order_delimiter_folded : _order_delimiter
println(io, delim, string(c.cell_id))
end
end
function save_notebook(notebook::Notebook, path::String)
open(path, "w") do io
save_notebook(io, notebook)
end
end
save_notebook(notebook::Notebook) = save_notebook(notebook, notebook.path)
"Load a notebook without saving it or creating a backup; returns a `Notebook`. REMEMBER TO CHANGE THE NOTEBOOK PATH after loading it to prevent it from autosaving and overwriting the original file."
function load_notebook_nobackup(io, path)::Notebook
firstline = String(readline(io))
if firstline != _notebook_header
error("File is not a Pluto.jl notebook")
end
file_VERSION_STR = readline(io)[3:end]
if file_VERSION_STR != PLUTO_VERSION_STR
# @info "Loading a notebook saved with Pluto $(file_VERSION_STR). This is Pluto $(PLUTO_VERSION_STR)."
end
collected_cells = Dict()
# ignore first bits of file
readuntil(io, _cell_id_delimiter)
last_read = ""
while !eof(io)
cell_id_str = String(readline(io))
if cell_id_str == "Cell order:"
break
else
cell_id = UUID(cell_id_str)
code_raw = String(readuntil(io, _cell_id_delimiter))
# change Windows line endings to Linux
code_normalised = replace(code_raw, "\r\n" => "\n")
# remove the cell appendix
code = code_normalised[1:prevind(code_normalised, end, length(_cell_suffix))]
read_cell = Cell(cell_id, code)
collected_cells[cell_id] = read_cell
end
end
ordered_cells = Cell[]
while !eof(io)
cell_id_str = String(readline(io))
o, c = startswith(cell_id_str, _order_delimiter),
if length(cell_id_str) >= 36
cell_id = let
UUID(cell_id_str[end - 35:end])
end
next_cell = collected_cells[cell_id]
next_cell.code_folded = startswith(cell_id_str, _order_delimiter_folded)
push!(ordered_cells, next_cell)
end
end
Notebook(ordered_cells, path)
end
function load_notebook_nobackup(path::String)::Notebook
local loaded
open(path, "r") do io
loaded = load_notebook_nobackup(io, path)
end
loaded
end
"Create a backup of the given file, load the file as a .jl Pluto notebook, save the loaded notebook, compare the two files, and delete the backup of the newly saved file is equal to the backup."
function load_notebook(path::String)::Notebook
backup_path = numbered_until_new(path; sep=".backup", suffix="", create_file=false)
# local backup_num = 1
# backup_path = path
# while isfile(backup_path)
# backup_path = path * ".backup" * string(backup_num)
# backup_num += 1
# end
readwrite(path, backup_path)
loaded = load_notebook_nobackup(path)
# Analyze cells so that the initial save is in topological order
update_caches!(loaded, loaded.cells)
loaded.topology = updated_topology(loaded.topology, loaded, loaded.cells)
save_notebook(loaded)
# Clear symstates if autorun/autofun is disabled. Otherwise running a single cell for the first time will also run downstream cells.
if get_pl_env("PLUTO_RUN_NOTEBOOK_ON_LOAD") != "true"
loaded.topology = NotebookTopology()
end
if only_versions_or_lineorder_differ(path, backup_path)
rm(backup_path)
else
@warn "Old Pluto notebook might not have loaded correctly. Backup saved to: " backup_path
end
loaded
end
function move_notebook(notebook::Notebook, newpath::String)
# Will throw exception and return if anything goes wrong, so at least one file is guaranteed to exist.
oldpath = notebook.path
save_notebook(notebook, oldpath)
save_notebook(notebook, newpath)
@assert only_versions_differ(oldpath, newpath)
notebook.path = newpath
rm(oldpath)
end
"Check if two savefiles are identical, up to their version numbers and a possible line shuffle.
If a notebook has not yet had all of its cells run, we can't deduce the topological cell order."
function only_versions_or_lineorder_differ(pathA::AbstractString, pathB::AbstractString)::Bool
Set(readlines(pathA)[3:end]) == Set(readlines(pathB)[3:end])
end
function only_versions_differ(pathA::AbstractString, pathB::AbstractString)::Bool
readlines(pathA)[3:end] == readlines(pathB)[3:end]
end