diff --git a/src/Atom.jl b/src/Atom.jl index 6cf4c10b..d730587d 100644 --- a/src/Atom.jl +++ b/src/Atom.jl @@ -1,5 +1,6 @@ __precompile__() +@doc read(joinpath(dirname(@__DIR__), "README.md"), String) module Atom using Juno, Lazy, JSON, MacroTools, Media, Base.StackTraces @@ -44,6 +45,7 @@ include("utils.jl") include("display/display.jl") include("progress.jl") include("eval.jl") +include("modules.jl") include("workspace.jl") include("repl.jl") include("docs.jl") diff --git a/src/comm.jl b/src/comm.jl index 52bc328b..f447f25b 100644 --- a/src/comm.jl +++ b/src/comm.jl @@ -83,6 +83,8 @@ function initialise(; welcome = false) welcome && @msg welcome() end +exit_on_sigint(on) = ccall(:jl_exit_on_sigint, Nothing, (Cint,), on) + """ serve(port; kws...) diff --git a/src/eval.jl b/src/eval.jl index 04c5aa84..8e394851 100644 --- a/src/eval.jl +++ b/src/eval.jl @@ -1,4 +1,4 @@ -using CodeTools, LNR, Media +using Media import REPL using Logging: with_logger @@ -6,44 +6,6 @@ using .Progress: JunoProgressLogger ends_with_semicolon(x) = REPL.ends_with_semicolon(split(x,'\n',keepempty = false)[end]) -LNR.cursor(data::AbstractDict) = cursor(data["row"], data["column"]) - -exit_on_sigint(on) = ccall(:jl_exit_on_sigint, Nothing, (Cint,), on) - -function modulenames(data, pos) - main = haskey(data, "module") ? data["module"] : - haskey(data, "path") ? CodeTools.filemodule(data["path"]) : - "Main" - main == "" && (main = "Main") - sub = CodeTools.codemodule(data["code"], pos) - main, sub -end - -# keeps the latest file that has been used for Main module scope -const MAIN_MODULE_LOCATION = Ref{Tuple{String, Int}}(moduledefinition(Main)) - -handle("module") do data - main, sub = modulenames(data, cursor(data)) - - mod = CodeTools.getmodule(main) - smod = CodeTools.getmodule(mod, sub) - - if main == "Main" && sub == "" - MAIN_MODULE_LOCATION[] = get!(data, "path", ""), data["row"] - end - - return d(:main => main, - :sub => sub, - :inactive => (mod==nothing), - :subInactive => smod==nothing) -end - -handle("allmodules") do - sort!([string(m) for m in CodeTools.allchildren(Main)]) -end - -isselection(data) = data["start"] ≠ data["stop"] - withpath(f, path) = CodeTools.withpath(f, path == nothing || isuntitled(path) ? nothing : path) diff --git a/src/goto.jl b/src/goto.jl index 4a7bb44a..44896763 100644 --- a/src/goto.jl +++ b/src/goto.jl @@ -165,10 +165,6 @@ function searchtoplevelitems(mod::Module, text::String, path::Nothing) return pathitemsmaps end -# TODO: -# use the module detection logic below for general module auto-detection, -# e.g.: `module` handler and such - # sub entry method function _searchtoplevelitems(mod::Module, pathitemsmaps::PathItemsMaps) entrypath, paths = modulefiles(mod) # Revise-like approach diff --git a/src/modules.jl b/src/modules.jl new file mode 100644 index 00000000..803f258d --- /dev/null +++ b/src/modules.jl @@ -0,0 +1,291 @@ +#= +finding all the included files for a module + +1. Revise-like approach + * mostly adapted from https://github.com/timholy/Revise.jl/tree/b0c5c864ea78b93caaa820cb9cfc45eca47f43ff + * only works for precompiled modules +2. CSTPraser-based approach + * static parsing -- works for all the files but costly + * TODO: excludes files in submodules when searched from the parent module + * TODO: looks for non-toplevel `include` calls +=# + +# Revise-like approach +# -------------------- + +using Base: PkgId, UUID + +""" + parentfile, included_files = modulefiles(mod::Module) + +Return the `parentfile` in which `mod` was defined, as well as a list of any +other files that were `include`d to define `mod`. If this operation is unsuccessful, +`(nothing, nothing)` is returned. +All files are returned as absolute paths. +""" +function modulefiles(mod::Module) + # NOTE: src_file_key stuff was removed when adapted + parentfile = String(first(methods(getfield(mod, :eval))).file) + id = Base.PkgId(mod) + if id.name == "Base" || Symbol(id.name) ∈ stdlib_names + parentfile = normpath(Base.find_source_file(parentfile)) + filedata = Base._included_files + else + use_compiled_modules() || return nothing, nothing # FIXME: support non-precompiled packages + _, filedata = pkg_fileinfo(id) + end + filedata === nothing && return nothing, nothing + included_files = filter(mf -> mf[1] == mod, filedata) + return fixpath(parentfile), [fixpath(mf[2]) for mf in included_files] +end + +# Fix paths to files that define Julia (base and stdlibs) +function fixpath( + filename::AbstractString; + badpath = basebuilddir, + goodpath = basepath("..") +) + startswith(filename, badpath) || return fullpath(normpath(filename)) # NOTE: `fullpath` added when adapted + filec = filename + relfilename = relpath(filename, badpath) + relfilename0 = relfilename + for strippath in (joinpath("usr", "share", "julia"),) + if startswith(relfilename, strippath) + relfilename = relpath(relfilename, strippath) + if occursin("stdlib", relfilename0) && !occursin("stdlib", relfilename) + relfilename = joinpath("stdlib", relfilename) + end + end + end + ffilename = normpath(joinpath(goodpath, relfilename)) + if (isfile(filename) & !isfile(ffilename)) + ffilename = normpath(filename) + end + fullpath(ffilename) # NOTE: `fullpath` added when adapted +end + +""" + basebuilddir + +Julia's top-level directory when Julia was built, as recorded by the entries in +`Base._included_files`. +""" +const basebuilddir = let # NOTE: changed from `begin` to `let` when adapted + sysimg = filter(x -> endswith(x[2], "sysimg.jl"), Base._included_files)[1][2] + dirname(dirname(sysimg)) +end + +use_compiled_modules() = Base.JLOptions().use_compiled_modules != 0 + +# For tracking Julia's own stdlibs +const stdlib_names = Set([ + :Base64, + :CRC32c, + :Dates, + :DelimitedFiles, + :Distributed, + :FileWatching, + :Future, + :InteractiveUtils, + :Libdl, + :LibGit2, + :LinearAlgebra, + :Logging, + :Markdown, + :Mmap, + :OldPkg, + :Pkg, + :Printf, + :Profile, + :Random, + :REPL, + :Serialization, + :SHA, + :SharedArrays, + :Sockets, + :SparseArrays, + :Statistics, + :SuiteSparse, + :Test, + :Unicode, + :UUIDs, +]) + +function pkg_fileinfo(id::PkgId) + uuid, name = id.uuid, id.name + # Try to find the matching cache file + paths = Base.find_all_in_cache_path(id) + sourcepath = Base.locate_package(id) + for path in paths + Base.stale_cachefile(sourcepath, path) === true && continue + provides, includes_requires = parse_cache_header(path) + mods_files_mtimes, _ = includes_requires + for (pkgid, buildid) in provides + if pkgid.uuid === uuid && pkgid.name == name + return path, mods_files_mtimes + end + end + end + return nothing, nothing +end + +# A near-copy of the same method in `base/loading.jl`. However, this retains the full module path to the file. +function parse_cache_header(f::IO) + modules = Vector{Pair{PkgId,UInt64}}() + while true + n = read(f, Int32) + n == 0 && break + sym = String(read(f, n)) # module name + uuid = UUID((read(f, UInt64), read(f, UInt64))) # pkg UUID + build_id = read(f, UInt64) # build UUID (mostly just a timestamp) + 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 + includes = Tuple{Module,String,Float64}[] + requires = Pair{Module,PkgId}[] + while true + n2 = read(f, Int32) + n2 == 0 && break + depname = String(read(f, n2)) + mtime = read(f, Float64) + n1 = read(f, Int32) + mod = (n1 == 0) ? Main : Base.root_module(modules[n1].first) + if n1 != 0 + # determine the complete module path + while true + n1 = read(f, Int32) + totbytes -= 4 + n1 == 0 && break + submodname = String(read(f, n1)) + mod = getfield(mod, Symbol(submodname)) + totbytes -= n1 + end + end + if depname[1] != '\0' + push!(includes, (mod, depname, mtime)) + end + totbytes -= 4 + 4 + n2 + 8 + end + @assert totbytes == 12 "header of cache file appears to be corrupt" + return modules, (includes, requires) +end + +function parse_cache_header(cachefile::String) + io = open(cachefile, "r") + try + !Base.isvalid_cache_header(io) && throw(ArgumentError("Invalid header in cache file $cachefile.")) + return parse_cache_header(io) + finally + close(io) + end +end + +# CSTParser-based approach +# ------------------------ + +""" + included_files = modulefiles(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. +""" +function modulefiles(entrypath::String, files = Vector{String}()) + isfile′(entrypath) || return files + + push!(files, entrypath) + + text = read(entrypath, String) + parsed = CSTParser.parse(text, true) + items = toplevelitems(parsed, text) + + for item in items + if item isa ToplevelCall + expr = item.expr + if isinclude(expr) + nextfile = expr.args[3].val + nextentrypath = joinpath(dirname(entrypath), nextfile) + isfile(nextentrypath) || continue + modulefiles(nextentrypath, files) + end + end + end + + return files +end + + +#= +find entry file of a module +=# + +""" + entrypath, line = moduledefinition(mod::Module) + +Returns an entry file of `mod`, and its definition line. + +!!! note + This function works for non-precompiled packages. +""" +function moduledefinition(mod::Module) # NOTE: added when adapted + evalmethod = first(methods(getfield(mod, :eval))) + parentfile = String(evalmethod.file) + line = evalmethod.line + id = Base.PkgId(mod) + if id.name == "Base" || id.name == "Core" || Symbol(id.name) ∈ stdlib_names # NOTE: "Core" is added when adapted + parentfile = normpath(Base.find_source_file(parentfile)) + end + fixpath(parentfile), line +end + + +#= +auto-module detection for the current file + +# TODO: +# use the same logics as finding module files for this auto-module detection +# ref: https://github.com/JunoLab/Juno.jl/issues/411 +=# + +using CodeTools, LNR + +LNR.cursor(data::AbstractDict) = cursor(data["row"], data["column"]) + +function modulenames(data, pos) + main = haskey(data, "module") ? data["module"] : + haskey(data, "path") ? CodeTools.filemodule(data["path"]) : + "Main" + main == "" && (main = "Main") + sub = CodeTools.codemodule(data["code"], pos) + main, sub +end + +# keeps the latest file that has been used for Main module scope +const MAIN_MODULE_LOCATION = Ref{Tuple{String, Int}}(moduledefinition(Main)) + +handle("module") do data + main, sub = modulenames(data, cursor(data)) + + mod = CodeTools.getmodule(main) + smod = CodeTools.getmodule(mod, sub) + + if main == "Main" && sub == "" + MAIN_MODULE_LOCATION[] = get!(data, "path", ""), data["row"] + end + + return d(:main => main, + :sub => sub, + :inactive => (mod==nothing), + :subInactive => smod==nothing) +end + + +#= +find all modules +=# + +handle("allmodules") do + sort!([string(m) for m in CodeTools.allchildren(Main)]) +end diff --git a/src/utils.jl b/src/utils.jl index 53872c6e..3f3e631e 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -231,189 +231,3 @@ uriopen(file, line = 0) = "atom://julia-client/?open=true&file=$(file)&line=$(li uridocs(mod, word) = "atom://julia-client/?docs=true&mod=$(mod)&word=$(word)" urigoto(mod, word) = "atom://julia-client/?goto=true&mod=$(mod)&word=$(word)" urimoduleinfo(mod) = "atom://julia-client/?moduleinfo=true&mod=$(mod)" - -#= -module file detections - -adapted from https://github.com/timholy/Revise.jl/tree/b0c5c864ea78b93caaa820cb9cfc45eca47f43ff -=# - -using Base: PkgId, UUID - -""" - parentfile, included_files = modulefiles(mod::Module) - -Return the `parentfile` in which `mod` was defined, as well as a list of any -other files that were `include`d to define `mod`. If this operation is unsuccessful, -`(nothing, nothing)` is returned. -All files are returned as absolute paths. -""" -function modulefiles(mod::Module) - # NOTE: src_file_key stuff was removed when adapted - parentfile = String(first(methods(getfield(mod, :eval))).file) - id = Base.PkgId(mod) - if id.name == "Base" || Symbol(id.name) ∈ stdlib_names - parentfile = normpath(Base.find_source_file(parentfile)) - filedata = Base._included_files - else - use_compiled_modules() || return nothing, nothing # FIXME: support non-precompiled packages - _, filedata = pkg_fileinfo(id) - end - filedata === nothing && return nothing, nothing - included_files = filter(mf -> mf[1] == mod, filedata) - return fixpath(parentfile), [fixpath(mf[2]) for mf in included_files] -end - -function moduledefinition(mod::Module) # NOTE: added when adapted - evalmethod = first(methods(getfield(mod, :eval))) - parentfile = String(evalmethod.file) - line = evalmethod.line - id = Base.PkgId(mod) - if id.name == "Base" || id.name == "Core" || Symbol(id.name) ∈ stdlib_names # NOTE: "Core" is added when adapted - parentfile = normpath(Base.find_source_file(parentfile)) - end - fixpath(parentfile), line -end - -# Fix paths to files that define Julia (base and stdlibs) -function fixpath( - filename::AbstractString; - badpath = basebuilddir, - goodpath = basepath("..") -) - startswith(filename, badpath) || return fullpath(normpath(filename)) # NOTE: `fullpath` added when adapted - filec = filename - relfilename = relpath(filename, badpath) - relfilename0 = relfilename - for strippath in (joinpath("usr", "share", "julia"),) - if startswith(relfilename, strippath) - relfilename = relpath(relfilename, strippath) - if occursin("stdlib", relfilename0) && !occursin("stdlib", relfilename) - relfilename = joinpath("stdlib", relfilename) - end - end - end - ffilename = normpath(joinpath(goodpath, relfilename)) - if (isfile(filename) & !isfile(ffilename)) - ffilename = normpath(filename) - end - fullpath(ffilename) # NOTE: `fullpath` added when adapted -end - -""" - basebuilddir - -Julia's top-level directory when Julia was built, as recorded by the entries in -`Base._included_files`. -""" -const basebuilddir = let # NOTE: changed from `begin` to `let` when adapted - sysimg = filter(x -> endswith(x[2], "sysimg.jl"), Base._included_files)[1][2] - dirname(dirname(sysimg)) -end - -use_compiled_modules() = Base.JLOptions().use_compiled_modules != 0 - -# For tracking Julia's own stdlibs -const stdlib_names = Set([ - :Base64, - :CRC32c, - :Dates, - :DelimitedFiles, - :Distributed, - :FileWatching, - :Future, - :InteractiveUtils, - :Libdl, - :LibGit2, - :LinearAlgebra, - :Logging, - :Markdown, - :Mmap, - :OldPkg, - :Pkg, - :Printf, - :Profile, - :Random, - :REPL, - :Serialization, - :SHA, - :SharedArrays, - :Sockets, - :SparseArrays, - :Statistics, - :SuiteSparse, - :Test, - :Unicode, - :UUIDs, -]) - -function pkg_fileinfo(id::PkgId) - uuid, name = id.uuid, id.name - # Try to find the matching cache file - paths = Base.find_all_in_cache_path(id) - sourcepath = Base.locate_package(id) - for path in paths - Base.stale_cachefile(sourcepath, path) === true && continue - provides, includes_requires = parse_cache_header(path) - mods_files_mtimes, _ = includes_requires - for (pkgid, buildid) in provides - if pkgid.uuid === uuid && pkgid.name == name - return path, mods_files_mtimes - end - end - end - return nothing, nothing -end - -# A near-copy of the same method in `base/loading.jl`. However, this retains the full module path to the file. -function parse_cache_header(f::IO) - modules = Vector{Pair{PkgId,UInt64}}() - while true - n = read(f, Int32) - n == 0 && break - sym = String(read(f, n)) # module name - uuid = UUID((read(f, UInt64), read(f, UInt64))) # pkg UUID - build_id = read(f, UInt64) # build UUID (mostly just a timestamp) - 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 - includes = Tuple{Module,String,Float64}[] - requires = Pair{Module,PkgId}[] - while true - n2 = read(f, Int32) - n2 == 0 && break - depname = String(read(f, n2)) - mtime = read(f, Float64) - n1 = read(f, Int32) - mod = (n1 == 0) ? Main : Base.root_module(modules[n1].first) - if n1 != 0 - # determine the complete module path - while true - n1 = read(f, Int32) - totbytes -= 4 - n1 == 0 && break - submodname = String(read(f, n1)) - mod = getfield(mod, Symbol(submodname)) - totbytes -= n1 - end - end - if depname[1] != '\0' - push!(includes, (mod, depname, mtime)) - end - totbytes -= 4 + 4 + n2 + 8 - end - @assert totbytes == 12 "header of cache file appears to be corrupt" - return modules, (includes, requires) -end - -function parse_cache_header(cachefile::String) - io = open(cachefile, "r") - try - !Base.isvalid_cache_header(io) && throw(ArgumentError("Invalid header in cache file $cachefile.")) - return parse_cache_header(io) - finally - close(io) - end -end diff --git a/test/goto.jl b/test/goto.jl index 3fd54b05..f9e3f377 100644 --- a/test/goto.jl +++ b/test/goto.jl @@ -1,5 +1,5 @@ @testset "goto symbols" begin - using Atom: modulegotoitems, realpath′, toplevelgotoitems, SYMBOLSCACHE, + using Atom: modulegotoitems, toplevelgotoitems, SYMBOLSCACHE, regeneratesymbols, methodgotoitems, globalgotoitems using CSTParser @@ -51,19 +51,18 @@ @testset "module goto" begin let item = modulegotoitems("Atom", Main)[1] - @test item.file == realpath′(joinpath(@__DIR__, "..", "src", "Atom.jl")) - @test item.line == 2 + @test item.file == joinpath′(atomjldir, "Atom.jl") + @test item.line == 3 end let item = modulegotoitems("Junk2", Main.Junk)[1] - @test item.file == joinpath(@__DIR__, "fixtures", "Junk.jl") + @test item.file == joinpath′(junkpath) @test item.line == 14 end end @testset "goto toplevel symbols" begin ## where Revise approach works, i.e.: precompiled modules - let dir = realpath′(joinpath(@__DIR__, "..", "src")) - path = joinpath(dir, "comm.jl") + let path = joinpath′(atomjldir, "comm.jl") text = read(path, String) mod = Atom key = "Atom" @@ -79,23 +78,8 @@ # check caching works @test haskey(SYMBOLSCACHE, key) - # check the Revise-like approach finds all the included files - let numfiles = 0 - debuggerpath = realpath′(joinpath(@__DIR__, "..", "src", "debugger")) - profilerpath = realpath′(joinpath(@__DIR__, "..", "src", "profiler")) - for (d, ds, fs) ∈ walkdir(dir) - if d ∈ (debuggerpath, profilerpath) - numfiles += 1 # debugger.jl / traceur.jl (in Atom module) - continue - end - for f ∈ fs - if endswith(f, ".jl") # .jl check is needed for travis, who creates hoge.cov files - numfiles += 1 - end - end - end - @test length(SYMBOLSCACHE[key]) == numfiles - end + # 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 @@ -106,11 +90,18 @@ # 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 ## where the Revise-like approach doesn't work, e.g. non-precompiled modules @@ -143,7 +134,7 @@ # handle dot accessors gracefully let # can access the non-exported (non-method) bindings in the other module - path = realpath′(joinpath(@__DIR__, "..", "src", "goto.jl")) + path = joinpath′(@__DIR__, "..", "src", "goto.jl") text = read(@__FILE__, String) items = Dict.(toplevelgotoitems("Atom.SYMBOLSCACHE", Main, text, @__FILE__)) @test !isempty(items) @@ -151,7 +142,7 @@ @test items[1][:text] == "SYMBOLSCACHE" # handle if a module is duplicated - path = realpath′(joinpath(@__DIR__, "..", "src", "comm.jl")) + path = joinpath′(@__DIR__, "..", "src", "comm.jl") text = read(path, String) items = Dict.(toplevelgotoitems("Atom.handlers", Atom, text, path)) @test !isempty(items) diff --git a/test/modules.jl b/test/modules.jl new file mode 100644 index 00000000..aac522f2 --- /dev/null +++ b/test/modules.jl @@ -0,0 +1,51 @@ +@testset "module utilties" begin + @testset "find module definition" begin + using Atom: moduledefinition + + let (path, line) = moduledefinition(Atom) + @test path == joinpath′(@__DIR__, "..", "src", "Atom.jl") + @test line == 4 + end + let (path, line) = moduledefinition(Junk) + @test path == joinpath′(@__DIR__, "fixtures", "Junk.jl") + @test line == 1 + end + let (path, line) = moduledefinition(Junk.Junk2) + @test path == joinpath′(@__DIR__, "fixtures", "Junk.jl") + @test line == 15 + end + end + + @testset "find module files" begin + using Atom: modulefiles + + ## Revise-like module file detection + # works for precompiled packages + let (parentfile, included_files) = modulefiles(Atom) + expected = Set(atommodfiles) + actual = Set((parentfile, included_files...)) + @test actual == expected + + # can't detect display/webio.jl + @test_broken webiofile in modfiles + end + + # fails for non-precompiled packages + @test_broken junkpath == modulefiles(Junk)[1] + + ## CSTPraser-based module file detection + let included_files = normpath.(modulefiles(joinpath′(atomjldir, "Atom.jl"))) + # 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) + + # can't look for non-toplevel `include` calls + @test_broken webiofile in included_files + end + end +end diff --git a/test/runtests.jl b/test/runtests.jl index 649e2d06..8809f505 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,20 +4,57 @@ using Lazy import JSON +joinpath′(files...) = Atom.fullpath(joinpath(files...)) + +atomjldir = joinpath′(@__DIR__, "..", "src") + +webiofile = joinpath′(atomjldir, "display", "webio.jl") + +# files in `Atom` module (except files in its submodules) +atommodfiles = let + files = [] + debuggerdir = joinpath′(atomjldir, "debugger") + profilerdir = joinpath′(atomjldir, "profiler") + for (d, ds, fs) in walkdir(atomjldir) + # NOTE: update directories below when you create an new submodule + # the 2 files below are in Atom module + if d == debuggerdir + push!(files, joinpath′(d, "debugger.jl")) + continue + end + if d == profilerdir + push!(files, joinpath′(d, "profiler.jl")) + push!(files, joinpath′(d, "traceur.jl")) + continue + end + for f in fs + # NOTE: currently both Revise-like and CSTPraser-based approach fails + # to detect display/webio.jl as a file in Atom module + f == "webio.jl" && continue + + # .jl check is needed for travis, who creates hoge.cov files + endswith(f, ".jl") && push!(files, joinpath′(d, f)) + end + end + files +end + # mock a listener Core.eval(Atom, Meta.parse("sock = IOBuffer()")) readmsg() = JSON.parse(String(take!(Atom.sock))) # mock Module -junkpath = joinpath(@__DIR__, "fixtures", "Junk.jl") +junkpath = joinpath′(@__DIR__, "fixtures", "Junk.jl") include(junkpath) -include("./misc.jl") # basics -include("./utils.jl") -include("./eval.jl") -include("./outline.jl") -include("./completions.jl") -include("./goto.jl") -include("./datatip.jl") -include("./workspace.jl") -include("./display.jl") + +include("misc.jl") # basics +include("utils.jl") +include("eval.jl") +include("modules.jl") +include("outline.jl") +include("completions.jl") +include("goto.jl") +include("datatip.jl") +include("workspace.jl") +include("display.jl")