diff --git a/src/datatip.jl b/src/datatip.jl index 1c2a62ed..8f7db288 100644 --- a/src/datatip.jl +++ b/src/datatip.jl @@ -1,7 +1,6 @@ #= -@TODO: -Use our own UI components for this: atom-ide-ui is already deprecated, ugly, not fully functional, and and... -Once we can come to handle links within datatips, we may want to append method tables as well +@TODO use our own UI components for this: +atom-ide-ui is already deprecated, ugly, not fully functional, and and... =# handle("datatip") do data @@ -25,7 +24,7 @@ function datatip(word, mod, path, column = 1, row = 1, startrow = 0, context = " ldt = localdatatip(word, column, row, startrow, context) isempty(ldt) || return datatip(ldt) - tdt = topleveldatatip(mod, word) + tdt = globaldatatip(mod, word) tdt !== nothing && return Dict(:error => false, :strings => tdt) return Dict(:error => true) # nothing hits @@ -36,7 +35,7 @@ datatip(dt::Int) = Dict(:error => false, :line => dt) datatip(dt::Vector{Int}) = datatip(dt[1]) function localdatatip(word, column, row, startrow, context) - word = first(split(word, '.')) # ignore dot accessors + word = first(split(word, '.')) # always ignore dot accessors position = row - startrow ls = locals(context, position, column) filter!(ls) do l @@ -56,7 +55,7 @@ function localdatatip(l, word, startrow) end end -function topleveldatatip(mod, word) +function globaldatatip(mod, word) docs = @errs getdocs(mod, word) docs isa EvalError && return nothing @@ -71,6 +70,9 @@ function topleveldatatip(mod, word) processdoc!(docs, docstr, datatip) + ml = methods(val) + processmethods!(ml, datatip) + return datatip end @@ -122,6 +124,20 @@ processval!(val::Function, docstr, datatip) = begin end processval!(::Undefined, docstr, datatip) = nothing +function processmethods!(ml, datatip) + ms = collect(ml) + isempty(ms) && return + + substr = s"\g in \g at \g" + msstr = map(ms) do m + s = replace(string(m), methodloc_regex => substr) + "
  • $s
  • " + end |> join + + name = ms[1].name + pushmarkdown!(datatip, "
    $name has **$(length(ms))** methods:
    ") +end + function pushmarkdown!(datatip, markdown) (markdown == "" || markdown == "\n") && return push!(datatip, Dict(:type => :markdown, :value => markdown)) diff --git a/src/display/methods.jl b/src/display/methods.jl index 12008d8a..ddb210ef 100644 --- a/src/display/methods.jl +++ b/src/display/methods.jl @@ -4,9 +4,11 @@ stripparams(t) = replace(t, r"\{([A-Za-z, ]*?)\}" => "") interpose(xs, y) = map(i -> iseven(i) ? xs[i÷2] : y, 2:2length(xs)) +const methodloc_regex = r"(?.+) in (?.+) at (?.+)$" + function view(m::Method) str = sprint(show, "text/html", m) - str = replace(str, r" in .* at .*$" => "") + str = replace(str, methodloc_regex => s"\g") str = string("", str, "") tv, decls, file, line = Base.arg_decl_parts(m) HTML(str), file == :null ? "not found" : Atom.baselink(string(file), line) diff --git a/src/goto.jl b/src/goto.jl index 8e8ea0dd..d360c0d0 100644 --- a/src/goto.jl +++ b/src/goto.jl @@ -32,7 +32,7 @@ function gotosymbol( localitems = localgotoitem(word, path, column, row, startrow, context) isempty(localitems) || return Dict( :error => false, - :items => map(Dict, localitems), + :items => map(Dict, localitems) ) end @@ -66,7 +66,7 @@ Dict(gotoitem::GotoItem) = Dict( ### local goto function localgotoitem(word, path, column, row, startrow, context) - word = first(split(word, '.')) # ignore dot accessors + word = first(split(word, '.')) # always ignore dot accessors position = row - startrow ls = locals(context, position, column) filter!(ls) do l @@ -86,31 +86,31 @@ localgotoitem(word, ::Nothing, column, row, startrow, context) = [] # when `path function globalgotoitems(word, mod, text, path) mod = getmodule(mod) - moduleitems = modulegotoitems(word, mod) - isempty(moduleitems) || return moduleitems + # strip a dot-accessed module if exists + identifiers = split(word, '.') + head = string(identifiers[1]) + if head ≠ word && 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) + end + + val = getfield′(mod, word) + val isa Module && return [GotoItem(val)] # module goto toplevelitems = toplevelgotoitems(word, mod, text, path) - # only append methods that are not caught by `toplevelgotoitems` + # append method gotos that are not caught by `toplevelgotoitems` + ml = methods(val) files = map(item -> item.file, toplevelitems) - methoditems = filter!(item -> item.file ∉ files, methodgotoitems(mod, word)) - + methoditems = filter!(item -> item.file ∉ files, methodgotoitems(ml)) append!(toplevelitems, methoditems) end ## module goto -function modulegotoitems(word, mod)::Vector{GotoItem} - mod = getfield′(mod, Symbol(word)) - return mod isa Module ? [GotoItem(mod)] : [] -end - function GotoItem(mod::Module) - file, line = if mod == Main - MAIN_MODULE_LOCATION[] - else - moduledefinition(mod) - end + file, line = mod == Main ? MAIN_MODULE_LOCATION[] : moduledefinition(mod) GotoItem(string(mod), file, line - 1) end @@ -120,16 +120,6 @@ const PathItemsMaps = Dict{String, Vector{ToplevelItem}} const SYMBOLSCACHE = Dict{String, PathItemsMaps}() function toplevelgotoitems(word, mod, text, path) - # strip a dot-accessed module if exists - identifiers = split(word, '.') - head = identifiers[1] - if head ≠ word && (val = getfield′(mod, string(head))) isa Module - # if `head` is a module, update `word` and `mod` - nextword = join(identifiers[2:end], '.') - nextmod = val - return toplevelgotoitems(nextword, nextmod, text, path) - end - key = string(mod) pathitemsmaps = if haskey(SYMBOLSCACHE, key) SYMBOLSCACHE[key] @@ -139,8 +129,8 @@ function toplevelgotoitems(word, mod, text, path) ismacro(word) && (word = lstrip(word, '@')) ret = Vector{GotoItem}() - for (path, items) ∈ pathitemsmaps - for item ∈ filter(item -> filtertoplevelitem(word, item), items) + for (path, items) in pathitemsmaps + for item in filter(item -> filtertoplevelitem(word, item), items) push!(ret, GotoItem(path, item)) end end @@ -169,7 +159,7 @@ end function _searchtoplevelitems(mod::Module, pathitemsmaps::PathItemsMaps) entrypath, paths = modulefiles(mod) # Revise-like approach if entrypath !== nothing - for p ∈ [entrypath; paths] + for p in [entrypath; paths] _searchtoplevelitems(p, pathitemsmaps) end else # if Revise-like approach fails, fallback to CSTParser-based approach @@ -188,14 +178,13 @@ function _searchtoplevelitems(path::String, pathitemsmaps::PathItemsMaps) push!(pathitemsmaps, pathitemsmap) end -# module-walk by CSTParser-based, looking for toplevel `installed` calls +# 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) - pathitemsmap = path => items - push!(pathitemsmaps, pathitemsmap) + push!(pathitemsmaps, path => items) - # looking for toplevel `installed` calls + # looking for toplevel `include` calls for item in items if item isa ToplevelCall expr = item.expr @@ -278,7 +267,7 @@ function regeneratesymbols() unloadedlen = length(unloaded) total = loadedlen + unloadedlen - for (i, mod) ∈ enumerate(Base.loaded_modules_array()) + for (i, mod) in enumerate(Base.loaded_modules_array()) try modstr = string(mod) modstr == "__PackagePrecompilationStatementModule" && continue # will cause error @@ -292,7 +281,7 @@ function regeneratesymbols() end end - for (i, pkg) ∈ enumerate(unloaded) + for (i, pkg) in enumerate(unloaded) try path = Base.find_package(pkg) text = read(path, String) @@ -311,27 +300,19 @@ end ## method goto -function methodgotoitems(mod, word)::Vector{GotoItem} - ms = @errs getmethods(mod, word) - if ms isa EvalError - [] - else - map(GotoItem, aggregatemethods(ms)) - end -end +methodgotoitems(ml) = map(GotoItem, aggregatemethods(ml)) # aggregate methods with default arguments to the ones with full arguments -aggregatemethods(f) = aggregatemethods(methods(f)) -aggregatemethods(ms::MethodList) = aggregatemethods(collect(ms)) -function aggregatemethods(ms::Vector{Method}) - ms = sort(ms, by = m -> m.nargs, rev = true) +function aggregatemethods(ml) + ms = collect(ml) + sort!(ms, by = m -> m.nargs, rev = true) unique(m -> (m.file, m.line), ms) end function GotoItem(m::Method) _, link = view(m) sig = sprint(show, m) - text = replace(sig, r" in .* at .*$" => "") + text = replace(sig, methodloc_regex => s"\g") file = link.file line = link.line - 1 secondary = join(link.contents) diff --git a/src/utils.jl b/src/utils.jl index 3f3e631e..59868fa0 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -111,6 +111,15 @@ end # string utilties # --------------- +""" + strlimit(str::AbstractString, limit::Int = 30, ellipsis::AbstractString = "…") + +Chops off `str` so that its _length_ doesn't exceed `limit`. The excessive part + will be replaced by `ellipsis`. + +!!! note + The length of returned string will _never_ exceed `limit`. +""" function strlimit(str::AbstractString, limit::Int = 30, ellipsis::AbstractString = "…") will_append = length(str) > limit @@ -130,7 +139,11 @@ end shortstr(val) = strlimit(string(val), 20) -# singleton type for undefined values +""" + Undefined + +singleton type representing undefined values +""" struct Undefined end # get utilities diff --git a/test/datatip.jl b/test/datatip.jl index 90258a9d..f00c6359 100644 --- a/test/datatip.jl +++ b/test/datatip.jl @@ -19,7 +19,7 @@ @test localdatatip("l", 4) == 3 # line end - # remove dot accessors + # ignore dot accessors let str = """ function withdots(expr::CSTParser.EXPR) bind = CSTParser.bindingof(expr.args[1]) @@ -68,8 +68,8 @@ end end - @testset "toplevel datatips" begin - using Atom: topleveldatatip + @testset "global datatips" begin + using Atom: globaldatatip ## method datatip @eval Main begin @@ -81,7 +81,7 @@ datatipmethodtest() = nothing end - let datatip = topleveldatatip("Main", "datatipmethodtest") + let datatip = globaldatatip("Main", "datatipmethodtest") @test datatip isa Vector firsttip = datatip[1] secondtip = datatip[2] @@ -94,7 +94,7 @@ ## variable datatip @eval Main datatipvariabletest = "this string should be shown in datatip" - let datatip = topleveldatatip("Main", "datatipvariabletest") + let datatip = globaldatatip("Main", "datatipvariabletest") @test datatip isa Vector firsttip = datatip[1] @test firsttip[:type] == :snippet diff --git a/test/goto.jl b/test/goto.jl index f68ad548..129483ec 100644 --- a/test/goto.jl +++ b/test/goto.jl @@ -1,8 +1,4 @@ @testset "goto symbols" begin - using Atom: modulegotoitems, toplevelgotoitems, SYMBOLSCACHE, - regeneratesymbols, methodgotoitems, globalgotoitems - using CSTParser - @testset "goto local symbols" begin let str = """ function localgotoitem(word, path, column, row, startRow, context) # L0 @@ -31,7 +27,7 @@ @test localgotoitem("l", 8)[:line] === 7 end - # remove dot accessors + # ignore dot accessors let str = """ function withdots(expr::CSTParser.EXPR) bind = CSTParser.bindingof(expr.args[1]) @@ -49,204 +45,199 @@ @test Atom.localgotoitem("word", nothing, 1, 1, 0, "") == [] end - @testset "module goto" begin - let item = modulegotoitems("Atom", Main)[1] - @test item.file == joinpath′(atomjldir, "Atom.jl") - @test item.line == 3 - end - let item = modulegotoitems("Junk2", Main.Junk)[1] - @test item.file == joinpath′(junkpath) - @test item.line == 14 - end - end + @testset "goto global symbols" begin + using Atom: globalgotoitems, toplevelgotoitems, SYMBOLSCACHE, + regeneratesymbols, methodgotoitems - @testset "goto toplevel symbols" begin - ## where Revise approach works, i.e.: precompiled modules - let path = joinpath′(atomjldir, "comm.jl") + ## strip a dot-accessed modules + let + path = joinpath′(@__DIR__, "..", "src", "comm.jl") text = read(path, String) - mod = Atom - key = "Atom" - word = "handlers" - - # basic - let items = toplevelgotoitems(word, mod, text, path) .|> Dict - @test !isempty(items) - @test items[1][:file] == path - @test items[1][:text] == word - end - - # check caching works - @test haskey(SYMBOLSCACHE, key) + items = Dict.(globalgotoitems("Atom.handlers", "Atom", text, path)) + @test !isempty(items) + @test items[1][:file] == path + @test items[1][:text] == "handlers" + items = Dict.(globalgotoitems("Main.Atom.handlers", "Atom", text, path)) + @test !isempty(items) + @test items[1][:file] == path + @test items[1][:text] == "handlers" - # check the Revise-like approach finds all files in Atom module - @test length(SYMBOLSCACHE[key]) == length(atommodfiles) + # 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__)) + @test !isempty(items) + @test items[1][:file] == path + @test items[1][:text] == "SYMBOLSCACHE" + end - # when `path` isn't given, i.e. via docpane / workspace - let items = toplevelgotoitems(word, mod, "", nothing) .|> Dict - @test !isempty(items) - @test items[1][:file] == path - @test items[1][:text] == word + @testset "goto modules" begin + let item = globalgotoitems("Atom", "Main", "", nothing)[1] + @test item.file == joinpath′(atomjldir, "Atom.jl") + @test item.line == 3 end - - # 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 - @test !isempty(items) - @test items[1][:file] == path - @test items[1][:text] == word + let item = globalgotoitems("Junk2", "Main.Junk", "", nothing)[1] + @test item.file == joinpath′(junkpath) + @test item.line == 14 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) 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 - @test !isempty(items) - @test items[1][:file] == path - @test items[1][:line] == 16 - @test items[1][:text] == word + @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 + @test !isempty(items) + @test items[1][:file] == path + @test items[1][:text] == word + end + + # check caching works + @test haskey(SYMBOLSCACHE, key) + + # check the Revise-like approach finds all files in Atom module + @test length(SYMBOLSCACHE[key]) == length(atommodfiles) + + # when `path` isn't given, i.e. via docpane / workspace + let items = toplevelgotoitems(word, mod, "", nothing) .|> Dict + @test !isempty(items) + @test items[1][:file] == path + @test items[1][:text] == word + end + + # 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 + @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) end - # check caching works - @test haskey(Atom.SYMBOLSCACHE, key) - - # when `path` isn't given, i.e.: via docpane / workspace - let items = toplevelgotoitems(word, mod, "", nothing) .|> Dict - @test !isempty(items) - @test items[1][:file] == path - @test items[1][:line] == 16 - @test items[1][:text] == word + ## 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 + @test !isempty(items) + @test items[1][:file] == path + @test items[1][:line] == 16 + @test items[1][:text] == word + end + + # check caching works + @test haskey(Atom.SYMBOLSCACHE, key) + + # when `path` isn't given, i.e.: via docpane / workspace + let items = toplevelgotoitems(word, mod, "", nothing) .|> Dict + @test !isempty(items) + @test items[1][:file] == path + @test items[1][:line] == 16 + @test items[1][:text] == word + end end end - # handle dot accessors gracefully - let - # can access the non-exported (non-method) bindings in the other module - path = joinpath′(@__DIR__, "..", "src", "goto.jl") - text = read(@__FILE__, String) - items = Dict.(toplevelgotoitems("Atom.SYMBOLSCACHE", Main, text, @__FILE__)) - @test !isempty(items) - @test items[1][:file] == path - @test items[1][:text] == "SYMBOLSCACHE" - - # handle if a module is duplicated - path = joinpath′(@__DIR__, "..", "src", "comm.jl") + @testset "updating toplevel symbols" begin + mod = "Main.Junk" + path = junkpath text = read(path, String) - items = Dict.(toplevelgotoitems("Atom.handlers", Atom, text, path)) - @test !isempty(items) - @test items[1][:file] == path - @test items[1][:text] == "handlers" - end + function updatesymbols(mod, text, path) + parsed = CSTParser.parse(text, true) + items = Atom.toplevelitems(parsed, text) + Atom.updatesymbols(text, mod, path, items) + end - # don't error on the fallback case - @test toplevelgotoitems("word", Main, "", nothing) == [] - end + # check there is no cache before updating + @test filter(SYMBOLSCACHE[mod][path]) do item + Atom.str_value(item.expr) == "toplevelval2" + end |> isempty - @testset "updating toplevel symbols" begin - mod = "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 + # mock updatesymbol handler + originallines = readlines(path) + newtext = join(originallines[1:end - 1], '\n') + word = "toplevelval2" + newtext *= "\n$word = :youshoulderaseme\nend" + updatesymbols(mod, newtext, path) - # check there is no cache before updating - @test filter(SYMBOLSCACHE[mod][path]) do item - Atom.str_value(item.expr) == "toplevelval2" - end |> isempty + # check the cache is updated + @test filter(SYMBOLSCACHE[mod][path]) do item + Atom.str_value(item.expr) == word + end |> !isempty - # mock updatesymbol handler - originallines = readlines(path) - newtext = join(originallines[1:end - 1], '\n') - word = "toplevelval2" - newtext *= "\n$word = :youshoulderaseme\nend" - updatesymbols(mod, newtext, path) - - # check the cache is updated - @test filter(SYMBOLSCACHE[mod][path]) do item - Atom.str_value(item.expr) == word - end |> !isempty + let items = toplevelgotoitems(word, mod, newtext, path) .|> Dict + @test !isempty(items) + @test items[1][:file] == path + @test items[1][:text] == "toplevelval2" + end - let items = toplevelgotoitems(word, mod, newtext, path) .|> Dict - @test !isempty(items) - @test items[1][:file] == path - @test items[1][:text] == "toplevelval2" + # re-update the cache + updatesymbols(mod, text, path) + @test filter(SYMBOLSCACHE[mod][path]) do item + Atom.str_value(item.expr) == word + end |> isempty end - # re-update the cache - updatesymbols(mod, text, path) - @test filter(SYMBOLSCACHE[mod][path]) do item - Atom.str_value(item.expr) == word - end |> isempty - end - - @testset "regenerating symbols" begin - # @info "––– caching symbols in loaded modules (only errors shown) ------" - # with_logger(ConsoleLogger(stderr, Base.CoreLogging.Warn)) do - regeneratesymbols() - # end - # @info "––– finished caching -------------------------------------------" - - @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 - end + @testset "regenerating symbols" begin + regeneratesymbols() - @testset "goto methods" begin - ## basic - # `Atom.handlemsg` is not defined with default args - let items = methodgotoitems("Main", "Atom.handlemsg") - @test length(items) === length(methods(Atom.handlemsg)) + @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 end - ## module awareness - let items = methodgotoitems("Atom", "handlemsg") - @test length(items) === length(methods(Atom.handlemsg)) - end + @testset "goto methods" begin + ## basic + let ms = methods(Atom.handlemsg) + @test length(methodgotoitems(ms)) === length(ms) + end - ## aggregate methods with default params - @eval Main function funcwithdefaultargs(args, defarg = "default") end + ## aggregate methods with default params + @eval Main function funcwithdefaultargs(args, defarg = "default") end - let items = methodgotoitems("Main", "funcwithdefaultargs") .|> Dict - # should be handled as an unique method - @test length(items) === 1 - # show a method with full arguments - @test "funcwithdefaultargs(args, defarg)" ∈ map(i -> i[:text], items) - end + let items = methodgotoitems(methods(funcwithdefaultargs)) .|> Dict + # should be handled as an unique method + @test length(items) === 1 + # show a method with full arguments + @test "funcwithdefaultargs(args, defarg)" in map(i -> i[:text], items) + end - @eval Main function funcwithdefaultargs(args::String, defarg = "default") end + @eval Main function funcwithdefaultargs(args::String, defarg = "default") end - let items = methodgotoitems("Main", "funcwithdefaultargs") .|> Dict - # should be handled as different methods - @test length(items) === 2 - # show methods with full arguments - @test "funcwithdefaultargs(args, defarg)" ∈ map(i -> i[:text], items) - @test "funcwithdefaultargs(args::String, defarg)" ∈ map(i -> i[:text], items) + let items = methodgotoitems(methods(funcwithdefaultargs)) .|> Dict + # should be handled as different methods + @test length(items) === 2 + # show methods with full arguments + @test "funcwithdefaultargs(args, defarg)" in map(i -> i[:text], items) + @test "funcwithdefaultargs(args::String, defarg)" in map(i -> i[:text], items) + end end - end - @testset "goto global symbols" begin # toplevel symbol goto & method goto - # both the original methods and the toplevel bindings that are overloaded - # in a context module should be shown + ## 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) @test length(items) === 2 - @test "isconst(m::Module, s::Symbol)" ∈ map(item -> item.text, items) # from Base - @test "Base.isconst(::JunkType)" ∈ map(item -> item.text, items) # from Junk + @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) == [] end end