From 56baf3beb9307d53594ccad45c0cb779db488c80 Mon Sep 17 00:00:00 2001 From: Fons van der Plas Date: Mon, 16 Mar 2020 16:03:21 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=BE=20Explorer:=20structs=20and=20loca?= =?UTF-8?q?ls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Project.toml | 2 +- assets/editor.html | 16 ++++-- src/ExploreExpression.jl | 106 +++++++++++++++++++++++++++++--------- test/ExploreExpression.jl | 68 +++++++++++++++++------- 4 files changed, 142 insertions(+), 50 deletions(-) diff --git a/Project.toml b/Project.toml index 8f94536fa1..59be668981 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 ", "Mikołaj Bochenski "] -version = "0.3.0" +version = "0.3.1" [deps] HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" diff --git a/assets/editor.html b/assets/editor.html index ac71278492..daca33a19e 100644 --- a/assets/editor.html +++ b/assets/editor.html @@ -22,6 +22,7 @@ body { margin: 0px; font-size: 17px; + overflow-anchor: none; } /* more sensible defaults for html tags: */ @@ -182,7 +183,6 @@ min-height: 25px; padding-left: 10px; padding-right: 10px; - overflow: auto; } code { @@ -495,9 +495,10 @@

} function updateLocalCellOutput(cellNode, mime, output, errormessage) { - console.log(mime) cellNode.classList.remove("running") + oldHeight = cellNode.querySelector("celloutput").scrollHeight + if (errormessage) { cellNode.querySelector("celloutput").innerHTML = "
" cellNode.querySelector("celloutput").querySelector("code").innerText = errormessage @@ -538,6 +539,13 @@

cellNode.querySelector("celloutput").querySelector("code").innerText = output } } + + newHeight = cellNode.querySelector("celloutput").scrollHeight + + focusedCell = document.querySelector("cell:focus-within") + if(focusedCell == cellNode){ + window.scrollBy(0, newHeight - oldHeight) + } } function updateLocalCellInput(byMe, uuid, code) { @@ -776,11 +784,9 @@

startDisconnectedBanner() } - console.log("update event received") - // console.log(event) + console.log("update received:") try { update = JSON.parse(event.data) - console.log("output deserialized") console.log(update) forMe = !(("notebookID" in update) && (update.notebookID != notebookID)) diff --git a/src/ExploreExpression.jl b/src/ExploreExpression.jl index c09f0c6f5c..d73e88357f 100644 --- a/src/ExploreExpression.jl +++ b/src/ExploreExpression.jl @@ -29,7 +29,27 @@ function union(a::ScopeState, b::ScopeState) end function ==(a::SymbolsState, b::SymbolsState) - return a.references == b.references && a.assignments == b.assignments + a.references == b.references && a.assignments == b.assignments +end + +function will_assign_global(assignee::Symbol, scopestate::ScopeState)::Bool + (scopestate.inglobalscope || assignee in scopestate.exposedglobals) && !(assignee in scopestate.hiddenglobals) +end + +function get_global_assignees(assignee_exprs, scopestate::ScopeState) + global_assignees = Set{Symbol}() + for ae in assignee_exprs + if isa(ae, Symbol) + will_assign_global(ae, scopestate) && push!(global_assignees, ae) + else + if ae.head == :(::) + will_assign_global(ae.args[1], scopestate) && push!(global_assignees, ae.args[1]) + else + @warn "Unknown assignee expression" + end + end + end + global_assignees end # We handle a list of function arguments separately. @@ -46,10 +66,12 @@ function extractfunctionarguments(funcdef::Expr)::Set{Symbol} if isa(a, Symbol) push!(argnames, a) elseif isa(a, Expr) - if a.head == :parameters || a.head == :tuple # second is for ((a,b),(c,d)) -> a*b*c*d stuff + if a.head == :(::) + push!(argnames, a.args[1]) + elseif a.head == :parameters || a.head == :tuple # second is for ((a,b),(c,d)) -> a*b*c*d stuff push!(argnames, extractfunctionarguments(a)...) elseif a.head == :kw || a.head == :(=) # first is for unnamed function arguments, second is for lambdas - push!(argnames, a.args[1]) + push!(argnames, extractfunctionarguments(a.args[1])...) end end end @@ -96,6 +118,9 @@ function explore!(ex::Expr, scopestate::ScopeState)::SymbolsState if ex.args[1].head == :tuple # (x, y) = (1, 23) ex.args[1].args + elseif ex.args[1].head == :(::) + # TODO: type is referenced + [ex.args[1].args[1]] elseif ex.args[1].head == :ref # TODO: what is the desired behaviour here? # right now, it registers no reference, and no assignment @@ -115,9 +140,7 @@ function explore!(ex::Expr, scopestate::ScopeState)::SymbolsState end val = ex.args[2] - global_assignees = filter(assignees) do assignee - scopestate.inglobalscope || assignee in scopestate.exposedglobals - end + global_assignees = get_global_assignees(assignees, scopestate) # If we are _not_ assigning a global variable for assignee in setdiff(assignees, global_assignees) @@ -144,7 +167,7 @@ function explore!(ex::Expr, scopestate::ScopeState)::SymbolsState operator = Symbol(string(ex.head)[1:end - 1]) expanded_expr = Expr(:(=), ex.args[1], Expr(:call, operator, ex.args[1], ex.args[2])) return explore!(expanded_expr, scopestate) - elseif ex.head == :let || ex.head == :for + elseif ex.head == :let || ex.head == :for || ex.head == :while # Creates local scope # Because we are entering a new scope, we create a copy of the current scope state, and run it through the expressions. @@ -156,6 +179,21 @@ function explore!(ex::Expr, scopestate::ScopeState)::SymbolsState end return symstate + elseif ex.head == :struct + # Creates local scope + + structname = assignee = if isa(ex.args[2], Symbol) + ex.args[2] + else + # We have: struct a <: b + ex.args[2].args[1] + # TODO: record reactive reference to type + end + structfields = ex.args[3].args + + equiv_func = Expr(:function, Expr(:call, structname, structfields...), Expr(:block, nothing)) + + return explore!(equiv_func, scopestate) elseif ex.head == :generator # Creates local scope @@ -167,36 +205,40 @@ function explore!(ex::Expr, scopestate::ScopeState)::SymbolsState elseif ex.head == :function # Creates local scope - funcname = assignee = ex.args[1].args[1] - funcargs = ex.args[1] + funcroot = if ex.args[1].head == :(::) + # TODO: record reactive reference to type + ex.args[1].args[1] + else + ex.args[1] + end - assigning_global = scopestate.inglobalscope || assignee in scopestate.exposedglobals - + funcname = assignee = funcroot.args[1] + + # is either [funcname] or [] + global_assignees = get_global_assignees([funcname], scopestate) # Because we are entering a new scope, we create a copy of the current scope state, and run it through the expressions. innerscopestate = deepcopy(scopestate) - innerscopestate.hiddenglobals = union(innerscopestate.hiddenglobals, extractfunctionarguments(funcargs)) + innerscopestate.hiddenglobals = union(innerscopestate.hiddenglobals, extractfunctionarguments(funcroot)) innerscopestate.inglobalscope = false for a in ex.args[2:end] innersymstate = explore!(a, innerscopestate) symstate = symstate ∪ innersymstate end - if assigning_global - scopestate.hiddenglobals = union(scopestate.hiddenglobals, [assignee]) - symstate.assignments = union(symstate.assignments, [assignee]) - end + scopestate.hiddenglobals = union(scopestate.hiddenglobals, global_assignees) + symstate.assignments = union(symstate.assignments, global_assignees) return symstate elseif ex.head == :(->) # Creates local scope - funcargs = ex.args[1] + funcroot = ex.args[1] # Because we are entering a new scope, we create a copy of the current scope state, and run it through the expressions. innerscopestate = deepcopy(scopestate) - innerscopestate.hiddenglobals = union(innerscopestate.hiddenglobals, extractfunctionarguments(funcargs)) + innerscopestate.hiddenglobals = union(innerscopestate.hiddenglobals, extractfunctionarguments(funcroot)) innerscopestate.inglobalscope = false for a in ex.args[2:end] innersymstate = explore!(a, innerscopestate) @@ -212,13 +254,13 @@ function explore!(ex::Expr, scopestate::ScopeState)::SymbolsState # global x = 1; # global x += 1; - # `globalised` is everything that comes after `global` + # where x can also be a tuple: + # global a,b = 1,2 globalisee = ex.args[1] if isa(globalisee, Symbol) scopestate.exposedglobals = union(scopestate.exposedglobals, [globalisee]) - # symstate.assignments = union(symstate.assignments, [globalisee]) elseif isa(globalisee, Expr) innerscopestate = deepcopy(scopestate) innerscopestate.inglobalscope = true @@ -228,6 +270,24 @@ function explore!(ex::Expr, scopestate::ScopeState)::SymbolsState @error "unknow global use" end + return symstate + elseif ex.head == :local + # Does not create scope + + # Logic similar to :global + localisee = ex.args[1] + + if isa(localisee, Symbol) + scopestate.hiddenglobals = union(scopestate.hiddenglobals, [localisee]) + elseif isa(localisee, Expr) + innerscopestate = deepcopy(scopestate) + innerscopestate.inglobalscope = false + innersymstate = explore!(localisee, innerscopestate) + symstate = symstate ∪ innersymstate + else + @error "unknow local use" + end + return symstate elseif ex.head == :tuple # Does not create scope @@ -249,10 +309,8 @@ function explore!(ex::Expr, scopestate::ScopeState)::SymbolsState if indexoffirstassignment !== nothing recursers = ex.args[indexoffirstassignment:end] - exposed = filter(ex.args[1:indexoffirstassignment - 1]) do a::Symbol - (scopestate.inglobalscope || a in scopestate.exposedglobals) && !(a in scopestate.hiddenglobals) - end - + exposed = get_global_assignees(ex.args[1:indexoffirstassignment - 1], scopestate) + scopestate.exposedglobals = union(scopestate.exposedglobals, exposed) symstate.assignments = union(symstate.assignments, exposed) end diff --git a/test/ExploreExpression.jl b/test/ExploreExpression.jl index ccbf52aa19..aa2612cb3e 100644 --- a/test/ExploreExpression.jl +++ b/test/ExploreExpression.jl @@ -2,9 +2,7 @@ using Test using Pluto import Pluto.ExploreExpression: SymbolsState, compute_symbolreferences -verbose = true - -function testee(expr, ref, def) +function testee(expr, ref, def, verbose=true) expected = SymbolsState(Set(ref), Set(def)) result = compute_symbolreferences(expr) if verbose && expected != result @@ -21,6 +19,10 @@ function testee(expr, ref, def) return expected == result end +# nowarn tests are for functionality that is not yet implemented +# (but we don't want any expressions to error/warn) +# Once the functionality has been implemented, they should be changed to normal tests + @testset "Explore Expressions" begin @testset "Basics" begin @test testee(:(a), [:a], []) @@ -30,25 +32,35 @@ end @test testee(:(x = 1 + y), [:+, :y], [:x]) @test testee(:(x = +(a...)), [:+, :a], [:x]) + @test_nowarn testee(:(x::Int64 = 3), [], [:x, :Int64], false) + end + @testset "Lists and structs" begin @test testee(:(1:3), [:(:)], []) @test testee(:(a[1:3,4]), [:a, :(:)], []) @test testee(:(a[1:3,4] = b[5]), [:b], []) @test testee(:(a.property), [:a], []) @test testee(:(a.property = 1), [], []) - + @test testee(:(struct a; c; d; end), [], [:a]) + + @test_nowarn testee(:(struct a <: b; c; d::Int64; end), [:b, :Int64], [:a], false) + end + @testset "Modifiers" begin + @test testee(:(a = a + 1), [:a, :(+)], [:a]) @test testee(:(a += 1), [:a, :(+)], [:a]) @test testee(:(a[1] += 1), [:a, :(+)], []) @test testee(:(x = let a = 1; a += b end), [:(+), :b], [:x]) - - @test testee(:(minimum(x) do (a, b); a + b end), [:(+), :x, :minimum], []) + end + @testset "`for` & `while`" begin @test testee(:(for k in 1:n; k + s; end), [:n, :s, :+, :(:)], []) @test testee(:(for k in 1:2, r in 3:4; global z = k + r; end), [:+, :(:)], [:z]) + @test testee(:(while k < 2; r = w; global z = k + r; end), [:k, :(<), :w, :+], [:z]) + end + @testset "Comprehensions" begin @test testee(:([sqrt(s) for s in 1:n]), [:sqrt, :n, :(:)], []) @test testee(:([s + j + r + m for s in 1:3 for j in 4:5 for (r, l) in [(1, 2)]]), [:+, :m, :(:)], []) - # @test testee(:([a for a in a]), [:a], []) - # @test testee(:(a = [a for a in a]), [:a], [:a]) - @test testee(:("a $(b = c)"), [:c], [:b]) + @test_nowarn testee(:([a for a in a]), [:a], [], false) + @test_nowarn testee(:(a = [a for a in a]), [:a], [:a], false) end @testset "Multiple expressions" begin @test testee(:(x = let r = 1; r + r end), [:+], [:x]) @@ -57,29 +69,42 @@ end @test testee(:((k = 2; 123)), [], [:k]) @test testee(:((a = 1; b = a + 1)), [:+], [:a, :b]) @test testee(:(let k = 2; 123 end), [], []) + + @test_nowarn testee(:(a::Int64, b::String = 1, "2"), [:Int64, :String], [:a, :b], false) end @testset "Functions" begin + @test testee(:(function g() r = 2; r end), [], [:g]) + @test testee(:(function f(x, y = 1; r, s = 3 + 3) r + s + x * y * z end), [:z, :+, :*], [:f]) + @test testee(:(function f(x) x * y * z end), [:y, :z, :*], [:f]) + @test testee(:(function f(x) x = x / 3; x end), [:/], [:f]) @test testee(:(f = x->x * y), [:y, :*], [:f]) @test testee(:(f = (x, y)->x * y), [:*], [:f]) @test testee(:(f(x, y = a + 1) = x * y * z), [:*, :z], [:f]) @test testee(:((((a, b), c), (d, e))->a * b * c * d * e * f), [:*, :f], []) @test testee(:(f = (x, y = a + 1)->x * y), [:*], [:f]) - @test testee(:(function g() r = 2; r end), [], [:g]) - @test testee(:(function f(x, y = 1; r, s = 3 + 3) r + s + x * y * z end), [:z, :+, :*], [:f]) - @test testee(:(function f(x, y = a; r, s = b) r + s + x * y * z end), [:z, :+, :*], [:f]) - @test testee(:(function f(x) x * y * z end), [:y, :z, :*], [:f]) - # @test testee(:(function f(x::T; k = 1) where T return x+1 end), [:+], [:f]) + @test testee(:(minimum(x) do (a, b); a + b end), [:(+), :x, :minimum], []) + + @test_nowarn testee(:(function f(y::Int64 = a)::String string(y) end), [:Int64, :String, :string], [:f], false) + @test_nowarn testee(:(function f(x::T; k = 1) where T return x + 1 end), [:+], [:f], false) end - @testset "Global exposure" begin + @testset "Scope modifiers" begin @test testee(:(let global a, b = 1, 2 end), [], [:a, :b]) - @test testee(:(let global k = 3; 123 end), [], [:k]) - @test testee(:(let global k; k = 2123 end), [], [:k]) - @test testee(:(let global a; b = 1 end), [], []) + @test testee(:(let global k = 3 end), [], [:k]) + @test testee(:(let global k += 3 end), [:+, :k], [:k]) + @test testee(:(let global k; k = 4 end), [], [:k]) + @test testee(:(let global k; b = 5 end), [], []) + + @test testee(:(begin local a, b = 1, 2 end), [], []) + @test testee(:(begin local k = 3 end), [], []) + @test testee(:(begin local k += 3 end), [:+], []) + @test testee(:(begin local k; k = 4 end), [], []) + @test testee(:(begin local k; b = 5 end), [], [:b]) + @test testee(:(function f(x) global k = x end), [], [:k, :f]) @test testee(:((begin x = 1 end, y)), [:y], [:x]) @test testee(:(x = let global a += 1 end), [:(+), :a], [:x, :a]) end - @testset "import/using" begin + @testset "`import` & `using`" begin @test testee(:(using Plots), [], [:Plots]) @test testee(:(using JSON, UUIDs), [], [:JSON, :UUIDs]) @test testee(:(import Pluto), [], [:Pluto]) @@ -90,6 +115,9 @@ end @test testee(:(html"a $(b = c)"), [Symbol("@html_str")], []) @test testee(:(md"a $(b = c)"), [Symbol("@md_str"), :c], [:b]) @test testee(:(md"a \$(b = c)"), [Symbol("@md_str")], []) - # @test testee(:(), [], []) + end + @testset "String interpolation" begin + @test testee(:("a $b"), [:b], []) + @test testee(:("a $(b = c)"), [:c], [:b]) end end \ No newline at end of file