271 lines
9.4 KiB
Julia
271 lines
9.4 KiB
Julia
module Autoloads
|
|
|
|
using TOML
|
|
using REPL
|
|
|
|
export Bindings, AutoDot, PkgAutoloads, add_autoloads!
|
|
|
|
function __init__()
|
|
if isinteractive()
|
|
read_named_envs!()
|
|
repl_apply_autoloads!()
|
|
end
|
|
end
|
|
|
|
struct Bindings
|
|
macros::Vector{Symbol}
|
|
callables::Vector{Symbol}
|
|
variables::Vector{Symbol}
|
|
end
|
|
|
|
struct AutoDot end
|
|
|
|
Bindings(; macros::Vector{<:Union{Symbol, String}}=Symbol[],
|
|
callables::Vector{Symbol}=Symbol[], variables=Symbol[]) =
|
|
Bindings(Symbol.(macros), callables, variables)
|
|
|
|
struct PkgAutoloads
|
|
pkg::Union{Symbol, Expr}
|
|
exported::Bindings
|
|
unexported::Union{Bindings, AutoDot}
|
|
partial::Bool
|
|
end
|
|
|
|
PkgAutoloads(pkg::Union{Symbol, Expr};
|
|
exported::Bindings = Bindings(Symbol[], Symbol[], Symbol[]),
|
|
unexported::Union{Bindings, AutoDot} = AutoDot(),
|
|
partial::Bool = false) =
|
|
PkgAutoloads(pkg, exported, unexported, partial)
|
|
|
|
function PkgAutoloads(mod::Module)
|
|
hasparent(u::Union, m::Module) =
|
|
hasparent(u.a, m) && hasparent(u.b, m)
|
|
hasparent(x::Any, m::Module) =
|
|
parentmodule(x) === m
|
|
macros = Symbol[]
|
|
callables = Symbol[]
|
|
variables = Symbol[]
|
|
for symb in names(mod)
|
|
obj = getglobal(mod, symb)
|
|
if !hasparent(obj, mod) || obj === mod
|
|
elseif startswith(String(symb), '@')
|
|
push!(macros, symb)
|
|
elseif obj isa Function || obj isa Type
|
|
push!(callables, symb)
|
|
else
|
|
push!(variables, symb)
|
|
end
|
|
end
|
|
PkgAutoloads(nameof(mod),
|
|
exported = Bindings(
|
|
macros, callables, variables))
|
|
end
|
|
|
|
const AUTOLOADS =
|
|
(sources = Dict{Symbol, PkgAutoloads}(),
|
|
pkgs = Dict{Symbol, Expr}(),
|
|
exported = (
|
|
macros = Dict{Symbol, Symbol}(),
|
|
callables = Dict{Symbol, Symbol}(),
|
|
variables = Dict{Symbol, Symbol}()),
|
|
bindings = (
|
|
macros = Dict{Symbol, Set{Symbol}}(),
|
|
callables = Dict{Symbol, Set{Symbol}}(),
|
|
variables = Dict{Symbol, Set{Symbol}}()),
|
|
ondotaccess = Set{Symbol}(),
|
|
loaded = Set{Symbol}(),
|
|
partloaded = Dict{Symbol, Set{Symbol}}())
|
|
|
|
function add_autoloads!(autos::PkgAutoloads)
|
|
!haskey(AUTOLOADS.sources, autos.pkg) ||
|
|
error("Autoloads for $(autos.pkg) have already been registered")
|
|
pkgsym, pkg = if autos.pkg isa Symbol
|
|
autos.pkg, Expr(:., autos.pkg)
|
|
elseif autos.pkg isa Expr &&
|
|
Meta.isexpr(autos.pkg, :(.)) &&
|
|
length(autos.pkg.args) == 2 &&
|
|
autos.pkg.args[1] isa Symbol &&
|
|
autos.pkg.args[2] isa QuoteNode &&
|
|
autos.pkg.args[2].value isa Symbol
|
|
autos.pkg.args[2].value,
|
|
Expr(:., autos.pkg.args[1], autos.pkg.args[2].value)
|
|
end
|
|
AUTOLOADS.sources[pkgsym] = autos
|
|
AUTOLOADS.pkgs[pkgsym] = pkg
|
|
autos.partial && (AUTOLOADS.partloaded[pkgsym] = Set{Symbol}())
|
|
AUTOLOADS.exported.variables[pkgsym] = pkgsym
|
|
for category in (:macros, :callables, :variables)
|
|
autodot = autos.unexported isa AutoDot
|
|
bindings = autodot || Set(getfield(autos.unexported, category))
|
|
slots = getfield(AUTOLOADS.exported, category)
|
|
for binding in getfield(autos.exported, category)
|
|
if haskey(slots, binding)
|
|
@warn "An autload for $(String(category)[1:end-1]) $binding already exists"
|
|
else
|
|
slots[binding] = pkgsym
|
|
end
|
|
autodot || push!(bindings, binding)
|
|
end
|
|
if !autodot && !isempty(bindings)
|
|
getfield(AUTOLOADS.bindings, category)[pkgsym] = bindings
|
|
end
|
|
end
|
|
end
|
|
|
|
const NAMED_ENVS = Vector{Pair{String, Vector{Symbol}}}()
|
|
|
|
function read_named_envs!()
|
|
empty!(NAMED_ENVS)
|
|
for depot in Base.DEPOT_PATH
|
|
envdir = joinpath(depot, "environments")
|
|
isdir(envdir) || continue
|
|
for env in readdir(envdir)
|
|
if !isnothing(match(r"^__", env))
|
|
elseif !isnothing(match(r"^v\d+\.\d+$", env))
|
|
elseif isfile(joinpath(envdir, env, "Project.toml"))
|
|
proj = open(TOML.parse, joinpath(envdir, env, "Project.toml"))
|
|
pkgs = get(proj, "deps", Dict{String, Any}()) |>
|
|
keys |> collect |> sort .|> Symbol
|
|
push!(NAMED_ENVS, '@' * env => pkgs)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function find_named_env(pkg::Symbol)
|
|
for (env, pkgs) in NAMED_ENVS
|
|
if pkg in pkgs
|
|
return env
|
|
end
|
|
end
|
|
end
|
|
|
|
function autoload_loadpkg(name::Symbol, thing::Union{Symbol, Nothing}=nothing)
|
|
if name ∉ AUTOLOADS.loaded && haskey(AUTOLOADS.pkgs, name)
|
|
AUTOLOADS.sources[name].partial && thing in AUTOLOADS.partloaded[name] &&
|
|
return
|
|
pkg = AUTOLOADS.pkgs[name]
|
|
if length(pkg.args) == 1 && isnothing(Base.find_package(String(first(pkg.args))))
|
|
named_env = find_named_env(name)
|
|
if !isnothing(named_env)
|
|
autoload_loadpkg_namedenv(name, named_env, thing)
|
|
else
|
|
@warn "Cannot autoload $name, as it is not availible in the current environment stack"
|
|
end
|
|
return
|
|
end
|
|
try
|
|
if !isnothing(thing) && AUTOLOADS.sources[name].partial
|
|
Core.eval(Main, Expr(:import, Expr(:., pkg.args..., thing)))
|
|
push!(AUTOLOADS.partloaded[name], thing)
|
|
else
|
|
Core.eval(Main, Expr(:using, pkg))
|
|
push!(AUTOLOADS.loaded, name)
|
|
end
|
|
nothing
|
|
catch err
|
|
@warn "Failed to automatically load $name" err
|
|
end
|
|
end
|
|
end
|
|
|
|
function autoload_loadpkg_namedenv(name::Symbol, env::String, thing::Union{Symbol, Nothing}=nothing)
|
|
pkg = AUTOLOADS.pkgs[name]
|
|
if Base.active_project() == Base.load_path_expand("@v#.#")
|
|
pushfirst!(LOAD_PATH, env)
|
|
try
|
|
if !isnothing(thing) && AUTOLOADS.sources[name].partial
|
|
Core.eval(Main, Expr(:import, Expr(:., pkg.args..., thing)))
|
|
push!(AUTOLOADS.partloaded[name], thing)
|
|
else
|
|
Core.eval(Main, Expr(:using, pkg))
|
|
push!(AUTOLOADS.loaded, name)
|
|
end
|
|
finally
|
|
popfirst!(LOAD_PATH)
|
|
end
|
|
else
|
|
@warn "Cannot autoload $name, but it is availible in the $env environment"
|
|
end
|
|
end
|
|
|
|
function autoload_scan!(ast::Expr, localbindings::Vector{Symbol}=Symbol[])
|
|
if Meta.isexpr(ast, :macrocall)
|
|
pkg = if first(ast.args) isa Symbol && first(ast.args) ∉ localbindings
|
|
get(AUTOLOADS.exported.macros, first(ast.args), nothing)
|
|
elseif Meta.isexpr(first(ast.args), :(.)) &&
|
|
length(first(ast.args).args) == 2 &&
|
|
(root = first(ast.args).args[1]) isa Symbol &&
|
|
first(ast.args).args[2] isa QuoteNode &&
|
|
(child = first(ast.args).args[2].value) isa Symbol &&
|
|
haskey(AUTOLOADS.bindings.macros, root) &&
|
|
haskey(AUTOLOADS.bindings.macros[root], child)
|
|
AUTOLOADS.bindings.macros[root]
|
|
end
|
|
if !isnothing(pkg) autoload_loadpkg(pkg, first(ast.args)) end
|
|
elseif Meta.isexpr(ast, :call)
|
|
pkg = if first(ast.args) isa Symbol && first(ast.args) ∉ localbindings
|
|
get(AUTOLOADS.exported.callables, first(ast.args), nothing)
|
|
elseif Meta.isexpr(first(ast.args), :(.)) &&
|
|
(root = first(ast.args).args[1]) isa Symbol &&
|
|
root ∈ AUTOLOADS.ondotaccess
|
|
root
|
|
elseif Meta.isexpr(first(ast.args), :(.)) &&
|
|
length(first(ast.args).args) == 2 &&
|
|
(root = first(ast.args).args[1]) isa Symbol &&
|
|
first(ast.args).args[2] isa QuoteNode &&
|
|
(child = first(ast.args).args[2].value) isa Symbol &&
|
|
haskey(AUTOLOADS.bindings.callables, root) &&
|
|
child ∈ AUTOLOADS.bindings.callables[root]
|
|
root
|
|
end
|
|
isnothing(pkg) || autoload_loadpkg(pkg, first(ast.args))
|
|
elseif Meta.isexpr(ast, :(.)) &&
|
|
length(ast.args) == 2 &&
|
|
(root = ast.args[1]) isa Symbol &&
|
|
ast.args[2] isa QuoteNode &&
|
|
(child = ast.args[2].value) isa Symbol &&
|
|
(root ∈ AUTOLOADS.ondotaccess ||
|
|
(haskey(AUTOLOADS.bindings.variables, root) &&
|
|
child ∈ AUTOLOADS.bindings.variables[root]) ||
|
|
(haskey(AUTOLOADS.bindings.callables, root) &&
|
|
child ∈ AUTOLOADS.bindings.callables[root]))
|
|
autoload_loadpkg(root)
|
|
elseif ast.head ∈ (:using, :import, :quote)
|
|
return
|
|
elseif Meta.isexpr(ast, :(=)) && first(ast.args) isa Symbol
|
|
push!(localbindings, first(ast.args))
|
|
end
|
|
for arg in ast.args
|
|
autoload_scan!(arg, localbindings)
|
|
end
|
|
end
|
|
|
|
function autoload_scan!(var::Symbol, localbindings::Vector{Symbol}=Symbol[])
|
|
if var ∉ localbindings && var ∉ names(Main)
|
|
pkg = @something(get(AUTOLOADS.exported.variables, var, nothing),
|
|
get(AUTOLOADS.exported.callables, var, nothing),
|
|
Some(nothing))
|
|
if !isnothing(pkg) autoload_loadpkg(pkg, var) end
|
|
end
|
|
end
|
|
|
|
autoload_scan!(::Any, ::Vector{Symbol}=Symbol[]) = nothing
|
|
|
|
function repl_scan_autoloads!(ast::Expr)
|
|
@static if VERSION < v"1.9"
|
|
autoload_scan!(ast)
|
|
else
|
|
if Base.active_module() == Main
|
|
autoload_scan!(ast)
|
|
end
|
|
end
|
|
ast
|
|
end
|
|
repl_scan_autoloads!(val::Any) = val
|
|
|
|
repl_apply_autoloads!() =
|
|
pushfirst!(REPL.repl_ast_transforms, repl_scan_autoloads!)
|
|
|
|
end
|