diff --git a/src/Atom.jl b/src/Atom.jl index d77ed850..a7a4e78a 100644 --- a/src/Atom.jl +++ b/src/Atom.jl @@ -22,12 +22,12 @@ function __init__() # HACK: overloading this allows us to open remote files InteractiveUtils.eval(quote - function InteractiveUtils.edit(path::AbstractString, line::Integer=0) + function InteractiveUtils.edit(path::AbstractString, line::Integer = 0) if endswith(path, ".jl") - f = Base.find_source_file(path) - f !== nothing && (path = f) + f = Base.find_source_file(path) + f !== nothing && (path = f) end - $(msg)("openFile", Base.abspath(path), line-1) + $(msg)("openFile", Base.abspath(path), line - 1) end end) end diff --git a/src/goto.jl b/src/goto.jl index d360c0d0..3c37ded8 100644 --- a/src/goto.jl +++ b/src/goto.jl @@ -1,5 +1,3 @@ -using CSTParser - handle("gotosymbol") do data @destruct [ word, @@ -17,13 +15,15 @@ handle("gotosymbol") do data gotosymbol( word, path, column, row, startRow, context, onlyGlobal, - mod, text + mod, text, ) end function gotosymbol( word, path = nothing, + # local context column = 1, row = 1, startrow = 0, context = "", onlyglobal = false, + # module context mod = "Main", text = "" ) try @@ -32,15 +32,15 @@ function gotosymbol( localitems = localgotoitem(word, path, column, row, startrow, context) isempty(localitems) || return Dict( :error => false, - :items => map(Dict, localitems) + :items => todict.(localitems) ) end # global goto - globalitems = globalgotoitems(word, mod, text, path) + globalitems = globalgotoitems(word, getmodule(mod), path, text) isempty(globalitems) || return Dict( :error => false, - :items => map(Dict, globalitems), + :items => todict.(globalitems), ) catch err return Dict(:error => true) @@ -56,7 +56,8 @@ struct GotoItem secondary::String GotoItem(text, file, line = 0, secondary = "") = new(text, normpath(file), line, secondary) end -Dict(gotoitem::GotoItem) = Dict( + +todict(gotoitem::GotoItem) = Dict( :text => gotoitem.text, :file => gotoitem.file, :line => gotoitem.line, @@ -79,32 +80,30 @@ function localgotoitem(word, path, column, row, startrow, context) GotoItem(text, path, line) end end -localgotoitem(word, ::Nothing, column, row, startrow, context) = [] # when `path` is not destructured +localgotoitem(word, ::Nothing, column, row, startrow, context) = [] # when called from docpane/workspace ### global goto - bundles toplevel gotos & method gotos -function globalgotoitems(word, mod, text, path) - mod = getmodule(mod) - +function globalgotoitems(word, mod, path, text) # strip a dot-accessed module if exists identifiers = split(word, '.') head = string(identifiers[1]) - if head ≠ word && getfield′(mod, head) isa Module + if head ≠ word && (nextmod = getfield′(mod, head)) isa Module # if `head` is a module, update `word` and `mod` nextword = join(identifiers[2:end], '.') - return globalgotoitems(nextword, head, text, path) + return globalgotoitems(nextword, nextmod, text, path) end val = getfield′(mod, word) val isa Module && return [GotoItem(val)] # module goto - toplevelitems = toplevelgotoitems(word, mod, text, path) + items = toplevelgotoitems(word, mod, path, text) # append method gotos that are not caught by `toplevelgotoitems` ml = methods(val) - files = map(item -> item.file, toplevelitems) + files = map(item -> item.file, items) methoditems = filter!(item -> item.file ∉ files, methodgotoitems(ml)) - append!(toplevelitems, methoditems) + append!(items, methoditems) end ## module goto @@ -117,18 +116,28 @@ end ## toplevel goto const PathItemsMaps = Dict{String, Vector{ToplevelItem}} + +""" + Atom.SYMBOLSCACHE + +"module" (`String`) ⟶ "path" (`String`) ⟶ "symbols" (`Vector{ToplevelItem}`) map. + +!!! note + "module" should be canonical, i.e.: should be identical to names that are + constructed from `string(mod::Module)`. +""" const SYMBOLSCACHE = Dict{String, PathItemsMaps}() -function toplevelgotoitems(word, mod, text, path) +function toplevelgotoitems(word, mod, path, text) key = string(mod) pathitemsmaps = if haskey(SYMBOLSCACHE, key) SYMBOLSCACHE[key] else - SYMBOLSCACHE[key] = searchtoplevelitems(mod, text, path) # caching + SYMBOLSCACHE[key] = collecttoplevelitems(mod, path, text) # caching end ismacro(word) && (word = lstrip(word, '@')) - ret = Vector{GotoItem}() + ret = [] for (path, items) in pathitemsmaps for item in filter(item -> filtertoplevelitem(word, item), items) push!(ret, GotoItem(path, item)) @@ -137,52 +146,57 @@ function toplevelgotoitems(word, mod, text, path) return ret end -# entry method -function searchtoplevelitems(mod::Module, text::String, path::String) - pathitemsmaps = PathItemsMaps() - if mod == Main # for `Main` module, always use the passed text - _searchtoplevelitems(text, path, pathitemsmaps) +# entry methods +function collecttoplevelitems(mod::Module, path::String, text::String) + return if mod == Main || isuntitled(path) + # for `Main` module and unsaved editors, always use CSTPraser-based approach + # with a given buffer text, and don't check module validity + __collecttoplevelitems(nothing, path, text) else - _searchtoplevelitems(mod, pathitemsmaps) + _collecttoplevelitems(mod) end - return pathitemsmaps -end - -# entry method when path is not deconstructured, e.g.: called from docpane/workspace -function searchtoplevelitems(mod::Module, text::String, path::Nothing) - pathitemsmaps = PathItemsMaps() - _searchtoplevelitems(mod, pathitemsmaps) - return pathitemsmaps end +# when `path === nothing`, e.g.: called from docpane/workspace +collecttoplevelitems(mod::Module, path::Nothing, text::String) = _collecttoplevelitems(mod) -# sub entry method -function _searchtoplevelitems(mod::Module, pathitemsmaps::PathItemsMaps) - entrypath, paths = modulefiles(mod) # Revise-like approach - if entrypath !== nothing - for p in [entrypath; paths] - _searchtoplevelitems(p, pathitemsmaps) - end +function _collecttoplevelitems(mod::Module) + entrypath, paths = modulefiles(mod) + return if entrypath !== nothing # Revise-like approach + __collecttoplevelitems(stripdotprefixes(string(mod)), [entrypath; paths]) else # if Revise-like approach fails, fallback to CSTParser-based approach - path, line = moduledefinition(mod) - text = read(path, String) - _searchtoplevelitems(text, path, pathitemsmaps) + entrypath, line = moduledefinition(mod) + __collecttoplevelitems(stripdotprefixes(string(mod)), entrypath) end end # module-walk via Revise-like approach -function _searchtoplevelitems(path::String, pathitemsmaps::PathItemsMaps) - text = read(path, String) - parsed = CSTParser.parse(text, true) - items = toplevelitems(parsed, text) - pathitemsmap = path => items - push!(pathitemsmaps, pathitemsmap) +function __collecttoplevelitems(mod::Union{Nothing, String}, paths::Vector{String}) + pathitemsmaps = PathItemsMaps() + + entrypath, paths = paths[1], paths[2:end] + + # ignore toplevel items outside of `mod` + items = toplevelitems(read(entrypath, String); mod = mod) + push!(pathitemsmaps, entrypath => items) + + # collect symbols in included files (always in `mod`) + for path in paths + items = toplevelitems(read(path, String); mod = mod, inmod = true) + push!(pathitemsmaps, path => items) + end + + pathitemsmaps end -# module-walk based on CSTParser, looking for toplevel `installed` calls -function _searchtoplevelitems(text::String, path::String, pathitemsmaps::PathItemsMaps) - parsed = CSTParser.parse(text, true) - items = toplevelitems(parsed, text) - push!(pathitemsmaps, path => items) +# module-walk based on CSTParser, looking for toplevel `included` calls +function __collecttoplevelitems(mod::Union{Nothing, String}, entrypath::String, pathitemsmaps::PathItemsMaps = PathItemsMaps(); inmod = false) + isfile′(entrypath) || return + text = read(entrypath, String) + __collecttoplevelitems(mod, entrypath, text, pathitemsmaps; inmod = inmod) +end +function __collecttoplevelitems(mod::Union{Nothing, String}, entrypath::String, text::String, pathitemsmaps::PathItemsMaps = PathItemsMaps(); inmod = false) + items = toplevelitems(text; mod = mod, inmod = inmod) + push!(pathitemsmaps, entrypath => items) # looking for toplevel `include` calls for item in items @@ -190,13 +204,15 @@ function _searchtoplevelitems(text::String, path::String, pathitemsmaps::PathIte expr = item.expr if isinclude(expr) nextfile = expr.args[3].val - nextpath = joinpath(dirname(path), nextfile) - isfile(nextpath) || continue - text = read(nextpath, String) - _searchtoplevelitems(text, nextpath, pathitemsmaps) + nextentrypath = joinpath(dirname(entrypath), nextfile) + isfile′(nextentrypath) || continue + # `nextentrypath` is always in `mod` + __collecttoplevelitems(mod, nextentrypath, pathitemsmaps; inmod = true) end end end + + pathitemsmaps end filtertoplevelitem(word, item::ToplevelItem) = false @@ -222,34 +238,43 @@ function GotoItem(path::String, bind::ToplevelBinding) text = str_value(sig) end line = bind.lines.start - 1 - secondary = path * ":" * string(line) + secondary = string(path, ":", line + 1) GotoItem(text, path, line, secondary) end function GotoItem(path::String, tupleh::ToplevelTupleH) expr = tupleh.expr text = str_value(expr) line = tupleh.lines.start - 1 - secondary = path * ":" * string(line) + secondary = string(path, ":", line + 1) GotoItem(text, path, line, secondary) end -## update toplevel symbols +## update toplevel symbols cache # NOTE: handled by the `updateeditor` handler in outline.jl -function updatesymbols(text, mod, path, items) - if haskey(SYMBOLSCACHE, mod) - push!(SYMBOLSCACHE[mod], path => items) # don't try to walk in a module - else - # initialize the cache if there is no cache - SYMBOLSCACHE[mod] = searchtoplevelitems(getmodule(mod), text, path) +function updatesymbols(mod, path::Nothing, text) end # fallback case +function updatesymbols(mod, path::String, text) + m = getmodule(mod) + + # initialize the cache if there is no previous one + if !haskey(SYMBOLSCACHE, mod) + SYMBOLSCACHE[mod] = collecttoplevelitems(m, path, text) end + + # ignore toplevel items outside of `mod` when `path` is an entry file + entrypath, _ = moduledefinition(m) + inmod = path != entrypath + items = toplevelitems(text; mod = stripdotprefixes(mod), inmod = inmod) + push!(SYMBOLSCACHE[mod], path => items) end -## generate toplevel symbols +## generate toplevel symbols cache + handle("regeneratesymbols") do with_logger(JunoProgressLogger()) do regeneratesymbols() end + nothing end function regeneratesymbols() @@ -269,13 +294,11 @@ function regeneratesymbols() for (i, mod) in enumerate(Base.loaded_modules_array()) try - modstr = string(mod) - modstr == "__PackagePrecompilationStatementModule" && continue # will cause error - pathitemsmap = PathItemsMaps() + key = string(mod) + key == "__PackagePrecompilationStatementModule" && continue # will cause error - @logmsg -1 "Symbols: $modstr ($i / $total)" progress=i/total _id=id - _searchtoplevelitems(mod, pathitemsmap) - SYMBOLSCACHE[modstr] = pathitemsmap + @logmsg -1 "Symbols: $key ($i / $total)" progress=i/total _id=id + SYMBOLSCACHE[key] = _collecttoplevelitems(mod) catch err @error err end @@ -283,13 +306,9 @@ function regeneratesymbols() for (i, pkg) in enumerate(unloaded) try - path = Base.find_package(pkg) - text = read(path, String) - pathitemsmap = PathItemsMaps() - @logmsg -1 "Symbols: $pkg ($(i + loadedlen) / $total)" progress=(i+loadedlen)/total _id=id - _searchtoplevelitems(text, path, pathitemsmap) - SYMBOLSCACHE[pkg] = pathitemsmap + path = Base.find_package(pkg) + SYMBOLSCACHE[pkg] = __collecttoplevelitems(pkg, path) catch err @error err end @@ -298,6 +317,19 @@ function regeneratesymbols() @info "Finished generating the symbols cache" progress=1 _id=id end +## clear toplevel symbols cache + +handle("clearsymbols") do + clearsymbols() + nothing +end + +function clearsymbols() + for key in keys(SYMBOLSCACHE) + delete!(SYMBOLSCACHE, key) + end +end + ## method goto methodgotoitems(ml) = map(GotoItem, aggregatemethods(ml)) diff --git a/src/modules.jl b/src/modules.jl index 803f258d..d3707545 100644 --- a/src/modules.jl +++ b/src/modules.jl @@ -113,7 +113,7 @@ const stdlib_names = Set([ function pkg_fileinfo(id::PkgId) uuid, name = id.uuid, id.name - # Try to find the matching cache file + # Try to find the matching cache file paths = Base.find_all_in_cache_path(id) sourcepath = Base.locate_package(id) for path in paths @@ -141,8 +141,8 @@ function parse_cache_header(f::IO) push!(modules, PkgId(uuid, sym) => build_id) end totbytes = read(f, Int64) # total bytes for file dependencies - # read the list of requirements - # and split the list into include and requires statements + # read the list of requirements + # and split the list into include and requires statements includes = Tuple{Module,String,Float64}[] requires = Pair{Module,PkgId}[] while true @@ -153,7 +153,7 @@ function parse_cache_header(f::IO) n1 = read(f, Int32) mod = (n1 == 0) ? Main : Base.root_module(modules[n1].first) if n1 != 0 - # determine the complete module path + # determine the complete module path while true n1 = read(f, Int32) totbytes -= 4 @@ -186,20 +186,20 @@ end # ------------------------ """ - included_files = modulefiles(entrypath::String)::Vector{String} + included_files = modulefiles(mod::String, entrypath::String)::Vector{String} -Returns all the files that can be reached via [`include`](@ref) calls from `entrypath`. -Note this function currently only looks for static toplevel calls (i.e. miss the calls - in not in toplevel scope), and can include files in the submodules as well. +Returns all the files in `mod` module that can be reached via [`include`](@ref) + calls from `entrypath`. +Note this function currently only looks for static toplevel calls (i.e. miss the + calls in non-toplevel scope). """ -function modulefiles(entrypath::String, files = Vector{String}()) +function modulefiles(mod::String, entrypath::String, files = Vector{String}(); inmod = false) isfile′(entrypath) || return files push!(files, entrypath) text = read(entrypath, String) - parsed = CSTParser.parse(text, true) - items = toplevelitems(parsed, text) + items = toplevelitems(text; mod = mod, inmod = inmod) for item in items if item isa ToplevelCall @@ -208,7 +208,8 @@ function modulefiles(entrypath::String, files = Vector{String}()) nextfile = expr.args[3].val nextentrypath = joinpath(dirname(entrypath), nextfile) isfile(nextentrypath) || continue - modulefiles(nextentrypath, files) + # `nextentrypath` is always in `mod` + modulefiles(mod, nextentrypath, files; inmod = true) end end end diff --git a/src/outline.jl b/src/outline.jl index 3c7eb36a..d23c8493 100644 --- a/src/outline.jl +++ b/src/outline.jl @@ -2,7 +2,7 @@ handle("updateeditor") do data @destruct [ text || "", mod || "Main", - path || "untitled", + path || nothing, updateSymbols || true ] = data @@ -14,16 +14,13 @@ handle("updateeditor") do data end # NOTE: update outline and symbols cache all in one go -function updateeditor(text, mod = "Main", path = "untitled", updateSymbols = true) - parsed = CSTParser.parse(text, true) - items = toplevelitems(parsed, text) - +function updateeditor(text, mod = "Main", path = nothing, updateSymbols = true) # update symbols cache # ref: https://github.com/JunoLab/Juno.jl/issues/407 - updateSymbols && updatesymbols(text, mod, path, items) + updateSymbols && updatesymbols(mod, path, text) # return outline - outline(items) + outline(toplevelitems(text)) end function outline(items) diff --git a/src/static/static.jl b/src/static/static.jl index c927536d..46c8defb 100644 --- a/src/static/static.jl +++ b/src/static/static.jl @@ -50,6 +50,9 @@ function isinclude(expr::CSTParser.EXPR) endswith(expr.args[3].val, ".jl") end +ismodule(expr::CSTParser.EXPR) = + expr.typ === CSTParser.ModuleH || expr.typ === CSTParser.BareModule + function isdoc(expr::CSTParser.EXPR) expr.typ === CSTParser.MacroCall && length(expr.args) >= 1 && diff --git a/src/static/toplevel.jl b/src/static/toplevel.jl index 849531de..abfff75d 100644 --- a/src/static/toplevel.jl +++ b/src/static/toplevel.jl @@ -2,7 +2,6 @@ Find toplevel items (bind / call) - downstreams: modules.jl, outline.jl, goto.jl -- TODO: crate `ToplevelScope` and allow to escape modules =# @@ -25,10 +24,32 @@ struct ToplevelTupleH <: ToplevelItem lines::UnitRange{Int} end -function toplevelitems(expr, text, items::Vector{ToplevelItem} = Vector{ToplevelItem}(), line = 1, pos = 1) +""" + toplevelitems(text; kwargs...)::Vector{ToplevelItem} + +Finds and returns toplevel "item"s (call and binding) in `text`. + +keyword arguments: +- `mod::Union{Nothing, String}`: if not `nothing` don't return items within modules + other than `mod`, otherwise enter into every module. +- `inmod::Bool`: if `true`, don't include toplevel items until it enters into `mod`. +""" +function toplevelitems(text; kwargs...) + parsed = CSTParser.parse(text, true) + _toplevelitems(text, parsed; kwargs...) +end + +function _toplevelitems( + text, expr, + items::Vector{ToplevelItem} = Vector{ToplevelItem}(), line = 1, pos = 1; + mod::Union{Nothing, String} = nothing, + inmod::Bool = false, +) + shouldadd = mod === nothing || inmod + # binding bind = CSTParser.bindingof(expr) - if bind !== nothing + if bind !== nothing && shouldadd lines = line:line+countlines(expr, text, pos, false) push!(items, ToplevelBinding(expr, bind, lines)) end @@ -36,18 +57,23 @@ function toplevelitems(expr, text, items::Vector{ToplevelItem} = Vector{Toplevel lines = line:line+countlines(expr, text, pos, false) # toplevel call - if iscallexpr(expr) + if iscallexpr(expr) && shouldadd push!(items, ToplevelCall(expr, lines, str_value_as_is(expr, text, pos))) end # destructure multiple returns - ismultiplereturn(expr) && push!(items, ToplevelTupleH(expr, lines)) + if ismultiplereturn(expr) && shouldadd + push!(items, ToplevelTupleH(expr, lines)) + end # look for more toplevel items in expr: - if shouldenter(expr) + if shouldenter(expr, mod) if expr.args !== nothing + if ismodule(expr) && shouldentermodule(expr, mod) + inmod = true + end for arg in expr.args - toplevelitems(arg, text, items, line, pos) + _toplevelitems(text, arg, items, line, pos; mod = mod, inmod = inmod) line += countlines(arg, text, pos) pos += arg.fullspan end @@ -56,11 +82,13 @@ function toplevelitems(expr, text, items::Vector{ToplevelItem} = Vector{Toplevel return items end -function shouldenter(expr::CSTParser.EXPR) +function shouldenter(expr::CSTParser.EXPR, mod::Union{Nothing, String}) !(scopeof(expr) !== nothing && !( expr.typ === CSTParser.FileH || - expr.typ === CSTParser.ModuleH || - expr.typ === CSTParser.BareModule || + (ismodule(expr) && shouldentermodule(expr, mod)) || isdoc(expr) )) end + +shouldentermodule(expr::CSTParser.EXPR, mod::Nothing) = true +shouldentermodule(expr::CSTParser.EXPR, mod::String) = expr.binding.name == mod diff --git a/src/utils.jl b/src/utils.jl index 59868fa0..1c14fe21 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -17,7 +17,14 @@ nonwritablefiles(files) = filter(!iswritablefile, files) include("path_matching.jl") -isuntitled(p) = occursin(r"^(\.\\|\./)?untitled-[\d\w]+(:\d+)?$", p) +""" + isuntitled(path::AbstractString) + +Checks if `path` represents an unsaved editor. +Usualy the string that follows `"untitled-"` is obtained from `editor.getBuffer().getId()`: + e.g. `path = "untitled-266305858c1298b906bed15ddad81cea"`. +""" +isuntitled(path::AbstractString) = occursin(r"^(\.\\|\./)?untitled-[\d\w]+(:\d+)?$", path) appendline(path, line) = line > 0 ? "$path:$line" : path @@ -139,6 +146,9 @@ end shortstr(val) = strlimit(string(val), 20) +"""used to strip parent module prefixes e.g.: `"Main.Junk" ⟶ "Junk"`""" +stripdotprefixes(str::AbstractString) = string(last(split(str, '.'))) + """ Undefined diff --git a/test/completions.jl b/test/completions.jl index 4c7eddfb..ce0cab44 100644 --- a/test/completions.jl +++ b/test/completions.jl @@ -163,5 +163,5 @@ end end # don't error on fallback case - @test Atom.localcompletions("", 1, 1) == [] + @test_nowarn @test Atom.localcompletions("", 1, 1) == [] end diff --git a/test/datatip.jl b/test/datatip.jl index f00c6359..a7ba5ecd 100644 --- a/test/datatip.jl +++ b/test/datatip.jl @@ -34,7 +34,7 @@ end # don't error on fallback case - @test Atom.localdatatip("word", 1, 1, 0, "") == [] + @test_nowarn @test Atom.localdatatip("word", 1, 1, 0, "") == [] end @testset "code block search" begin diff --git a/test/fixtures/Junk.jl b/test/fixtures/Junk.jl index 16e731a2..0687404a 100644 --- a/test/fixtures/Junk.jl +++ b/test/fixtures/Junk.jl @@ -12,22 +12,12 @@ macro immacro(expr) end end -module Junk2 end - const toplevelval = "you should jump to me !" # mock overloaded method struct JunkType end Base.isconst(::JunkType) = false -"""im a doc in Junk""" -const imwithdoc = nothing - -baremodule BareJunk - -"""im a doc in BareJunk""" -const imwithdoc = nothing - -end +include("SubJunks.jl") end diff --git a/test/fixtures/SubJunks.jl b/test/fixtures/SubJunks.jl new file mode 100644 index 00000000..77bbd67b --- /dev/null +++ b/test/fixtures/SubJunks.jl @@ -0,0 +1,16 @@ +"""im a doc in Junk""" +const imwithdoc = nothing + +module SubJunk + +"""im a doc in SubJunk""" +const imwithdoc = nothing + +end + +baremodule BareJunk + +"""im a doc in BareJunk""" +const imwithdoc = nothing + +end diff --git a/test/goto.jl b/test/goto.jl index 129483ec..55a71218 100644 --- a/test/goto.jl +++ b/test/goto.jl @@ -1,4 +1,6 @@ @testset "goto symbols" begin + using Atom: todict + @testset "goto local symbols" begin let str = """ function localgotoitem(word, path, column, row, startRow, context) # L0 @@ -15,7 +17,7 @@ end # L11 end # L12 """, - localgotoitem(word, line) = Atom.localgotoitem(word, "path", Inf, line + 1, 0, str)[1] |> Dict + localgotoitem(word, line) = Atom.localgotoitem(word, "path", Inf, line + 1, 0, str)[1] |> todict let item = localgotoitem("row", 2) @test item[:line] === 0 @@ -35,29 +37,29 @@ return val end """, - localgotoitem(word, line) = Atom.localgotoitem(word, "path", Inf, line + 1, 0, str)[1] |> Dict + localgotoitem(word, line) = Atom.localgotoitem(word, "path", Inf, line + 1, 0, str)[1] |> todict @test localgotoitem("expr.args", 1)[:line] === 0 @test localgotoitem("bind.val", 2)[:line] === 1 end # don't error on fallback case - @test Atom.localgotoitem("word", nothing, 1, 1, 0, "") == [] + @test_nowarn @test Atom.localgotoitem("word", nothing, 1, 1, 0, "") == [] end @testset "goto global symbols" begin - using Atom: globalgotoitems, toplevelgotoitems, SYMBOLSCACHE, - regeneratesymbols, methodgotoitems + using Atom: globalgotoitems, toplevelgotoitems, SYMBOLSCACHE, updatesymbols, + clearsymbols, regeneratesymbols, methodgotoitems ## strip a dot-accessed modules let path = joinpath′(@__DIR__, "..", "src", "comm.jl") text = read(path, String) - items = Dict.(globalgotoitems("Atom.handlers", "Atom", text, path)) + items = todict.(globalgotoitems("Atom.handlers", Atom, path, text)) @test !isempty(items) @test items[1][:file] == path @test items[1][:text] == "handlers" - items = Dict.(globalgotoitems("Main.Atom.handlers", "Atom", text, path)) + items = todict.(globalgotoitems("Main.Atom.handlers", Atom, path, text)) @test !isempty(items) @test items[1][:file] == path @test items[1][:text] == "handlers" @@ -65,33 +67,32 @@ # can access the non-exported (non-method) bindings in the other module path = joinpath′(@__DIR__, "..", "src", "goto.jl") text = read(@__FILE__, String) - items = Dict.(globalgotoitems("Atom.SYMBOLSCACHE", "Main", text, @__FILE__)) + items = todict.(globalgotoitems("Atom.SYMBOLSCACHE", Main, @__FILE__, text)) @test !isempty(items) @test items[1][:file] == path @test items[1][:text] == "SYMBOLSCACHE" end @testset "goto modules" begin - let item = globalgotoitems("Atom", "Main", "", nothing)[1] - @test item.file == joinpath′(atomjldir, "Atom.jl") - @test item.line == 3 + let item = globalgotoitems("Atom", Main, nothing, "")[1] |> todict + @test item[:file] == joinpath′(atomjldir, "Atom.jl") + @test item[:line] == 3 end - let item = globalgotoitems("Junk2", "Main.Junk", "", nothing)[1] - @test item.file == joinpath′(junkpath) - @test item.line == 14 + let item = globalgotoitems("SubJunk", Junk, nothing, "")[1] |> todict + @test item[:file] == subjunkspath + @test item[:line] == 3 end end @testset "goto toplevel symbols" begin ## where Revise approach works, i.e.: precompiled modules let path = joinpath′(atomjldir, "comm.jl") - text = read(path, String) mod = Atom key = "Atom" word = "handlers" # basic - let items = toplevelgotoitems(word, mod, text, path) .|> Dict + let items = todict.(toplevelgotoitems(word, mod, path, "")) @test !isempty(items) @test items[1][:file] == path @test items[1][:text] == word @@ -104,7 +105,7 @@ @test length(SYMBOLSCACHE[key]) == length(atommodfiles) # when `path` isn't given, i.e. via docpane / workspace - let items = toplevelgotoitems(word, mod, "", nothing) .|> Dict + let items = todict.(toplevelgotoitems(word, mod, nothing, "")) @test !isempty(items) @test items[1][:file] == path @test items[1][:text] == word @@ -113,31 +114,28 @@ # same as above, but without any previous cache -- falls back to CSTPraser-based module-walk delete!(SYMBOLSCACHE, key) - let items = toplevelgotoitems(word, mod, "", nothing) .|> Dict + let items = toplevelgotoitems(word, mod, nothing, "") .|> todict @test !isempty(items) @test items[1][:file] == path @test items[1][:text] == word end # check CSTPraser-based module-walk finds all the included files - # currently broken: - # - files in submodules are included - # - webio.jl is excluded since `include("webio.jl")` is a toplevel call - @test_broken length(SYMBOLSCACHE[key]) == length(atommoddir) + # NOTE: webio.jl is excluded since `include("webio.jl")` is a toplevel call + @test length(SYMBOLSCACHE[key]) == length(atommodfiles) end ## where the Revise-like approach doesn't work, e.g. non-precompiled modules let path = junkpath - text = read(path, String) mod = Main.Junk key = "Main.Junk" word = "toplevelval" - # basic - let items = toplevelgotoitems(word, mod, text, path) .|> Dict + # basic -- no need to pass a buffer text + let items = toplevelgotoitems(word, mod, path, "") .|> todict @test !isempty(items) @test items[1][:file] == path - @test items[1][:line] == 16 + @test items[1][:line] == 14 @test items[1][:text] == word end @@ -145,27 +143,50 @@ @test haskey(Atom.SYMBOLSCACHE, key) # when `path` isn't given, i.e.: via docpane / workspace - let items = toplevelgotoitems(word, mod, "", nothing) .|> Dict + let items = toplevelgotoitems(word, mod, nothing, "") .|> todict @test !isempty(items) @test items[1][:file] == path - @test items[1][:line] == 16 + @test items[1][:line] == 14 @test items[1][:text] == word end end + + ## don't include bindings outside of a module + let path = subjunkspath + text = read(subjunkspath, String) + mod = Main.Junk.SubJunk + word = "imwithdoc" + + items = toplevelgotoitems(word, mod, path, text) .|> todict + @test length(items) === 1 + if length(items) === 1 + @test items[1][:file] == path + @test items[1][:line] == 6 + @test items[1][:text] == word + end + end + + ## `Main` module -- use a passed buffer text + let path = joinpath′(@__DIR__, "runtests.jl") + text = read(path, String) + mod = Main + word = "atomjldir" + + items = toplevelgotoitems(word, mod, path, text) .|> todict + @test !isempty(items) + @test items[1][:file] == path + @test items[1][:line] == 5 + @test items[1][:text] == word + end end @testset "updating toplevel symbols" begin - mod = "Main.Junk" + # check there is no cache before updating + mod = Main.Junk + key = "Main.Junk" path = junkpath text = read(path, String) - function updatesymbols(mod, text, path) - parsed = CSTParser.parse(text, true) - items = Atom.toplevelitems(parsed, text) - Atom.updatesymbols(text, mod, path, items) - end - - # check there is no cache before updating - @test filter(SYMBOLSCACHE[mod][path]) do item + @test filter(SYMBOLSCACHE[key][path]) do item Atom.str_value(item.expr) == "toplevelval2" end |> isempty @@ -174,33 +195,42 @@ newtext = join(originallines[1:end - 1], '\n') word = "toplevelval2" newtext *= "\n$word = :youshoulderaseme\nend" - updatesymbols(mod, newtext, path) + updatesymbols(key, path, newtext) # check the cache is updated - @test filter(SYMBOLSCACHE[mod][path]) do item + @test filter(SYMBOLSCACHE[key][path]) do item Atom.str_value(item.expr) == word end |> !isempty - let items = toplevelgotoitems(word, mod, newtext, path) .|> Dict + let items = toplevelgotoitems(word, mod, path, newtext) .|> todict @test !isempty(items) @test items[1][:file] == path @test items[1][:text] == "toplevelval2" end # re-update the cache - updatesymbols(mod, text, path) - @test filter(SYMBOLSCACHE[mod][path]) do item + updatesymbols(key, path, text) + @test filter(SYMBOLSCACHE[key][path]) do item Atom.str_value(item.expr) == word end |> isempty + + # don't error on fallback case + @test_nowarn @test updatesymbols(key, nothing, text) === nothing end - @testset "regenerating symbols" begin + @testset "regenerating toplevel symbols" begin regeneratesymbols() @test haskey(SYMBOLSCACHE, "Base") @test length(keys(SYMBOLSCACHE["Base"])) > 100 @test haskey(SYMBOLSCACHE, "Example") # cache symbols even if not loaded - @test toplevelgotoitems("hello", "Example", "", nothing) |> !isempty + @test toplevelgotoitems("hello", Example, "", nothing) |> !isempty + end + + @testset "clear toplevel symbols" begin + clearsymbols() + + @test length(keys(SYMBOLSCACHE)) === 0 end @testset "goto methods" begin @@ -212,7 +242,7 @@ ## aggregate methods with default params @eval Main function funcwithdefaultargs(args, defarg = "default") end - let items = methodgotoitems(methods(funcwithdefaultargs)) .|> Dict + let items = methodgotoitems(methods(funcwithdefaultargs)) .|> todict # should be handled as an unique method @test length(items) === 1 # show a method with full arguments @@ -221,7 +251,7 @@ @eval Main function funcwithdefaultargs(args::String, defarg = "default") end - let items = methodgotoitems(methods(funcwithdefaultargs)) .|> Dict + let items = methodgotoitems(methods(funcwithdefaultargs)) .|> todict # should be handled as different methods @test length(items) === 2 # show methods with full arguments @@ -231,13 +261,13 @@ end ## both the original methods and the toplevel bindings that are overloaded in a context module should be shown - let items = globalgotoitems("isconst", "Main.Junk", "", nothing) + let items = globalgotoitems("isconst", Main.Junk, nothing, "") @test length(items) === 2 @test "isconst(m::Module, s::Symbol)" in map(item -> item.text, items) # from Base @test "Base.isconst(::JunkType)" in map(item -> item.text, items) # from Junk end ## don't error on the fallback case - @test globalgotoitems("word", "Main", "", nothing) == [] + @test_nowarn @test globalgotoitems("word", Main, nothing, "") == [] end end diff --git a/test/modules.jl b/test/modules.jl index 7272f4d2..8e92eb34 100644 --- a/test/modules.jl +++ b/test/modules.jl @@ -3,16 +3,16 @@ using Atom: moduledefinition let (path, line) = moduledefinition(Atom) - @test path == joinpath′(@__DIR__, "..", "src", "Atom.jl") + @test path == atommodfile @test line == 4 end let (path, line) = moduledefinition(Junk) - @test path == joinpath′(@__DIR__, "fixtures", "Junk.jl") + @test path == junkpath @test line == 1 end - let (path, line) = moduledefinition(Junk.Junk2) - @test path == joinpath′(@__DIR__, "fixtures", "Junk.jl") - @test line == 15 + let (path, line) = moduledefinition(Junk.SubJunk) + @test path == subjunkspath + @test line == 4 end end @@ -34,15 +34,15 @@ @test_broken junkpath == modulefiles(Junk)[1] ## CSTPraser-based module file detection - let included_files = normpath.(modulefiles(joinpath′(atomjldir, "Atom.jl"))) + let included_files = normpath.(modulefiles("Atom", atommodfile)) # finds all the files in Atom module except display/webio.jl for f in atommodfiles f == webiofile && continue @test f in included_files end - # can't exclude files in the submodules - @test_broken length(atommodfiles) == length(included_files) + # only finds files in a module -- exclude files in the submodules + @test length(atommodfiles) == length(included_files) # can't look for non-toplevel `include` calls @test_broken webiofile in included_files diff --git a/test/outline.jl b/test/outline.jl index 1e36530b..2d5bc78e 100644 --- a/test/outline.jl +++ b/test/outline.jl @@ -1,9 +1,5 @@ @testset "outline" begin - function outline(str) - parsed = CSTParser.parse(str, true) - items = Atom.toplevelitems(parsed, str) - Atom.outline(items) - end + outline(text) = Atom.outline(Atom.toplevelitems(text)) let str = """ module Foo diff --git a/test/runtests.jl b/test/runtests.jl index 27b4336e..c445c69b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,10 +1,10 @@ -using Atom, Test, JSON, Logging, CSTParser +using Atom, Test, JSON, Logging, CSTParser, Example joinpath′(files...) = Atom.fullpath(joinpath(files...)) atomjldir = joinpath′(@__DIR__, "..", "src") - +atommodfile = joinpath′(atomjldir, "Atom.jl") webiofile = joinpath′(atomjldir, "display", "webio.jl") # files in `Atom` module (except files in its submodules) @@ -42,6 +42,7 @@ readmsg() = JSON.parse(String(take!(Atom.sock))) # mock Module junkpath = joinpath′(@__DIR__, "fixtures", "Junk.jl") +subjunkspath = joinpath′(@__DIR__, "fixtures", "SubJunks.jl") include(junkpath) # basics diff --git a/test/static/static.jl b/test/static/static.jl index 0a32b7b5..2b720dd7 100644 --- a/test/static/static.jl +++ b/test/static/static.jl @@ -1,8 +1,7 @@ @testset "static analysis" begin - # TODO - # @testset "toplevel items" begin - # include("toplevel.jl") - # end + @testset "toplevel items" begin + include("toplevel.jl") + end @testset "local bindings" begin include("local.jl") diff --git a/test/static/toplevel.jl b/test/static/toplevel.jl new file mode 100644 index 00000000..bea6ca1c --- /dev/null +++ b/test/static/toplevel.jl @@ -0,0 +1,24 @@ +@testset "module validation" begin + using Atom: toplevelitems + + path = subjunkspath + text = read(path, String) + + # basic -- finds every toplevel items with default arguments + @test filter(toplevelitems(text)) do item + item isa Atom.ToplevelBinding && + item.bind.name == "imwithdoc" + end |> length === 3 + + # don't enter non-target modules, e.g.: submodules + @test filter(toplevelitems(text; mod = "Junk", inmod = true)) do item + item isa Atom.ToplevelBinding && + item.bind.name == "imwithdoc" + end |> length === 1 # should only find the `imwithdoc` in Junk module + + # don't include items outside of a module + @test filter(toplevelitems(text; mod = "SubJunk", inmod = false)) do item + item isa Atom.ToplevelBinding && + item.bind.name == "imwithdoc" + end |> length === 1 # should only find the `imwithdoc` in SubJunk module +end diff --git a/test/workspace.jl b/test/workspace.jl index 63b450aa..445eb48b 100644 --- a/test/workspace.jl +++ b/test/workspace.jl @@ -19,7 +19,7 @@ end # recoginise submodule - let items = filter(i -> i[:name] == :Junk2, items) + let items = filter(i -> i[:name] == :SubJunk, items) @test !isempty(items) @test items[1][:type] == "module" @test items[1][:icon] == "icon-package"