|
| 1 | +module REPLExt |
| 2 | + |
| 3 | +using Markdown, UUIDs, Dates |
| 4 | + |
| 5 | +import REPL |
| 6 | +import .REPL: LineEdit, REPLCompletions, TerminalMenus |
| 7 | + |
| 8 | +import Pkg |
| 9 | +import .Pkg: linewrap, pathrepr, compat, can_fancyprint, printpkgstyle, PKGMODE_PROJECT |
| 10 | +using .Pkg: Types, Operations, API, Registry, Resolve, REPLMode |
| 11 | + |
| 12 | +using .REPLMode: Statement, CommandSpec, Command, prepare_cmd, tokenize, core_parse, SPECS, api_options, parse_option, api_options, is_opt, wrap_option |
| 13 | + |
| 14 | +using .Types: Context, PkgError, pkgerror, EnvCache |
| 15 | + |
| 16 | + |
| 17 | +include("completions.jl") |
| 18 | +include("compat.jl") |
| 19 | + |
| 20 | +###################### |
| 21 | +# REPL mode creation # |
| 22 | +###################### |
| 23 | + |
| 24 | +struct PkgCompletionProvider <: LineEdit.CompletionProvider end |
| 25 | + |
| 26 | +function LineEdit.complete_line(c::PkgCompletionProvider, s) |
| 27 | + partial = REPL.beforecursor(s.input_buffer) |
| 28 | + full = LineEdit.input_string(s) |
| 29 | + ret, range, should_complete = completions(full, lastindex(partial)) |
| 30 | + return ret, partial[range], should_complete |
| 31 | +end |
| 32 | + |
| 33 | +prev_project_file = nothing |
| 34 | +prev_project_timestamp = nothing |
| 35 | +prev_prefix = "" |
| 36 | + |
| 37 | +function projname(project_file::String) |
| 38 | + project = try |
| 39 | + Types.read_project(project_file) |
| 40 | + catch |
| 41 | + nothing |
| 42 | + end |
| 43 | + if project === nothing || project.name === nothing |
| 44 | + name = basename(dirname(project_file)) |
| 45 | + else |
| 46 | + name = project.name::String |
| 47 | + end |
| 48 | + for depot in Base.DEPOT_PATH |
| 49 | + envdir = joinpath(depot, "environments") |
| 50 | + if startswith(abspath(project_file), abspath(envdir)) |
| 51 | + return "@" * name |
| 52 | + end |
| 53 | + end |
| 54 | + return name |
| 55 | +end |
| 56 | + |
| 57 | +function promptf() |
| 58 | + global prev_project_timestamp, prev_prefix, prev_project_file |
| 59 | + project_file = try |
| 60 | + Types.find_project_file() |
| 61 | + catch |
| 62 | + nothing |
| 63 | + end |
| 64 | + prefix = "" |
| 65 | + if project_file !== nothing |
| 66 | + if prev_project_file == project_file && prev_project_timestamp == mtime(project_file) |
| 67 | + prefix = prev_prefix |
| 68 | + else |
| 69 | + project_name = projname(project_file) |
| 70 | + if project_name !== nothing |
| 71 | + if textwidth(project_name) > 30 |
| 72 | + project_name = first(project_name, 27) * "..." |
| 73 | + end |
| 74 | + prefix = "($(project_name)) " |
| 75 | + prev_prefix = prefix |
| 76 | + prev_project_timestamp = mtime(project_file) |
| 77 | + prev_project_file = project_file |
| 78 | + end |
| 79 | + end |
| 80 | + end |
| 81 | + if Pkg.OFFLINE_MODE[] |
| 82 | + prefix = "$(prefix)[offline] " |
| 83 | + end |
| 84 | + return "$(prefix)pkg> " |
| 85 | +end |
| 86 | + |
| 87 | +do_cmds(repl::REPL.AbstractREPL, input::String) = do_cmds(repl, prepare_cmd(input)) |
| 88 | + |
| 89 | +function do_cmds(repl::REPL.AbstractREPL, commands::Vector{Command}) |
| 90 | + try |
| 91 | + return REPLMode.do_cmds(commands, repl.t.out_stream) |
| 92 | + catch err |
| 93 | + if err isa PkgError || err isa Resolve.ResolverError |
| 94 | + Base.display_error(repl.t.err_stream, ErrorException(sprint(showerror, err)), Ptr{Nothing}[]) |
| 95 | + else |
| 96 | + Base.display_error(repl.t.err_stream, err, Base.catch_backtrace()) |
| 97 | + end |
| 98 | + end |
| 99 | +end |
| 100 | + |
| 101 | +# Set up the repl Pkg REPLMode |
| 102 | +function create_mode(repl::REPL.AbstractREPL, main::LineEdit.Prompt) |
| 103 | + pkg_mode = LineEdit.Prompt(promptf; |
| 104 | + prompt_prefix = repl.options.hascolor ? Base.text_colors[:blue] : "", |
| 105 | + prompt_suffix = "", |
| 106 | + complete = PkgCompletionProvider(), |
| 107 | + sticky = true) |
| 108 | + |
| 109 | + pkg_mode.repl = repl |
| 110 | + hp = main.hist |
| 111 | + hp.mode_mapping[:pkg] = pkg_mode |
| 112 | + pkg_mode.hist = hp |
| 113 | + |
| 114 | + search_prompt, skeymap = LineEdit.setup_search_keymap(hp) |
| 115 | + prefix_prompt, prefix_keymap = LineEdit.setup_prefix_keymap(hp, pkg_mode) |
| 116 | + |
| 117 | + pkg_mode.on_done = (s, buf, ok) -> begin |
| 118 | + ok || return REPL.transition(s, :abort) |
| 119 | + input = String(take!(buf)) |
| 120 | + REPL.reset(repl) |
| 121 | + do_cmds(repl, input) |
| 122 | + REPL.prepare_next(repl) |
| 123 | + REPL.reset_state(s) |
| 124 | + s.current_mode.sticky || REPL.transition(s, main) |
| 125 | + end |
| 126 | + |
| 127 | + mk = REPL.mode_keymap(main) |
| 128 | + |
| 129 | + shell_mode = nothing |
| 130 | + for mode in repl.interface.modes |
| 131 | + if mode isa LineEdit.Prompt |
| 132 | + mode.prompt == "shell> " && (shell_mode = mode) |
| 133 | + end |
| 134 | + end |
| 135 | + |
| 136 | + repl_keymap = Dict() |
| 137 | + if shell_mode !== nothing |
| 138 | + let shell_mode=shell_mode |
| 139 | + repl_keymap[';'] = function (s,o...) |
| 140 | + if isempty(s) || position(LineEdit.buffer(s)) == 0 |
| 141 | + buf = copy(LineEdit.buffer(s)) |
| 142 | + LineEdit.transition(s, shell_mode) do |
| 143 | + LineEdit.state(s, shell_mode).input_buffer = buf |
| 144 | + end |
| 145 | + else |
| 146 | + LineEdit.edit_insert(s, ';') |
| 147 | + end |
| 148 | + end |
| 149 | + end |
| 150 | + end |
| 151 | + |
| 152 | + b = Dict{Any,Any}[ |
| 153 | + skeymap, repl_keymap, mk, prefix_keymap, LineEdit.history_keymap, |
| 154 | + LineEdit.default_keymap, LineEdit.escape_defaults |
| 155 | + ] |
| 156 | + pkg_mode.keymap_dict = LineEdit.keymap(b) |
| 157 | + return pkg_mode |
| 158 | +end |
| 159 | + |
| 160 | +function repl_init(repl::REPL.AbstractREPL) |
| 161 | + main_mode = repl.interface.modes[1] |
| 162 | + pkg_mode = create_mode(repl, main_mode) |
| 163 | + push!(repl.interface.modes, pkg_mode) |
| 164 | + keymap = Dict{Any,Any}( |
| 165 | + ']' => function (s,args...) |
| 166 | + if isempty(s) || position(LineEdit.buffer(s)) == 0 |
| 167 | + buf = copy(LineEdit.buffer(s)) |
| 168 | + LineEdit.transition(s, pkg_mode) do |
| 169 | + LineEdit.state(s, pkg_mode).input_buffer = buf |
| 170 | + end |
| 171 | + else |
| 172 | + LineEdit.edit_insert(s, ']') |
| 173 | + end |
| 174 | + end |
| 175 | + ) |
| 176 | + main_mode.keymap_dict = LineEdit.keymap_merge(main_mode.keymap_dict, keymap) |
| 177 | + return |
| 178 | +end |
| 179 | + |
| 180 | +const REG_WARNED = Ref{Bool}(false) |
| 181 | + |
| 182 | +function try_prompt_pkg_add(pkgs::Vector{Symbol}) |
| 183 | + ctx = try |
| 184 | + Context() |
| 185 | + catch |
| 186 | + # Context() will error if there isn't an active project. |
| 187 | + # If we can't even do that, exit early. |
| 188 | + return false |
| 189 | + end |
| 190 | + if isempty(ctx.registries) |
| 191 | + if !REG_WARNED[] |
| 192 | + printstyled(ctx.io, " │ "; color=:green) |
| 193 | + printstyled(ctx.io, "Attempted to find missing packages in package registries but no registries are installed.\n") |
| 194 | + printstyled(ctx.io, " └ "; color=:green) |
| 195 | + printstyled(ctx.io, "Use package mode to install a registry. `pkg> registry add` will install the default registries.\n\n") |
| 196 | + REG_WARNED[] = true |
| 197 | + end |
| 198 | + return false |
| 199 | + end |
| 200 | + available_uuids = [Types.registered_uuids(ctx.registries, String(pkg)) for pkg in pkgs] # vector of vectors |
| 201 | + filter!(u -> all(!isequal(Operations.JULIA_UUID), u), available_uuids) # "julia" is in General but not installable |
| 202 | + isempty(available_uuids) && return false |
| 203 | + available_pkgs = pkgs[isempty.(available_uuids) .== false] |
| 204 | + isempty(available_pkgs) && return false |
| 205 | + resp = try |
| 206 | + plural1 = length(pkgs) == 1 ? "" : "s" |
| 207 | + plural2 = length(available_pkgs) == 1 ? "a package" : "packages" |
| 208 | + plural3 = length(available_pkgs) == 1 ? "is" : "are" |
| 209 | + plural4 = length(available_pkgs) == 1 ? "" : "s" |
| 210 | + missing_pkg_list = length(pkgs) == 1 ? String(pkgs[1]) : "[$(join(pkgs, ", "))]" |
| 211 | + available_pkg_list = length(available_pkgs) == 1 ? String(available_pkgs[1]) : "[$(join(available_pkgs, ", "))]" |
| 212 | + msg1 = "Package$(plural1) $(missing_pkg_list) not found, but $(plural2) named $(available_pkg_list) $(plural3) available from a registry." |
| 213 | + for line in linewrap(msg1, io = ctx.io, padding = length(" │ ")) |
| 214 | + printstyled(ctx.io, " │ "; color=:green) |
| 215 | + println(ctx.io, line) |
| 216 | + end |
| 217 | + printstyled(ctx.io, " │ "; color=:green) |
| 218 | + println(ctx.io, "Install package$(plural4)?") |
| 219 | + msg2 = string("add ", join(available_pkgs, ' ')) |
| 220 | + for (i, line) in pairs(linewrap(msg2; io = ctx.io, padding = length(string(" | ", promptf())))) |
| 221 | + printstyled(ctx.io, " │ "; color=:green) |
| 222 | + if i == 1 |
| 223 | + printstyled(ctx.io, promptf(); color=:blue) |
| 224 | + else |
| 225 | + print(ctx.io, " "^length(promptf())) |
| 226 | + end |
| 227 | + println(ctx.io, line) |
| 228 | + end |
| 229 | + printstyled(ctx.io, " └ "; color=:green) |
| 230 | + Base.prompt(stdin, ctx.io, "(y/n/o)", default = "y") |
| 231 | + catch err |
| 232 | + if err isa InterruptException # if ^C is entered |
| 233 | + println(ctx.io) |
| 234 | + return false |
| 235 | + end |
| 236 | + rethrow() |
| 237 | + end |
| 238 | + if isnothing(resp) # if ^D is entered |
| 239 | + println(ctx.io) |
| 240 | + return false |
| 241 | + end |
| 242 | + resp = strip(resp) |
| 243 | + lower_resp = lowercase(resp) |
| 244 | + if lower_resp in ["y", "yes"] |
| 245 | + API.add(string.(available_pkgs)) |
| 246 | + elseif lower_resp in ["o"] |
| 247 | + editable_envs = filter(v -> v != "@stdlib", LOAD_PATH) |
| 248 | + option_list = String[] |
| 249 | + keybindings = Char[] |
| 250 | + shown_envs = String[] |
| 251 | + # We use digits 1-9 as keybindings in the env selection menu |
| 252 | + # That's why we can display at most 9 items in the menu |
| 253 | + for i in 1:min(length(editable_envs), 9) |
| 254 | + env = editable_envs[i] |
| 255 | + expanded_env = Base.load_path_expand(env) |
| 256 | + |
| 257 | + isnothing(expanded_env) && continue |
| 258 | + |
| 259 | + n = length(option_list) + 1 |
| 260 | + push!(option_list, "$(n): $(pathrepr(expanded_env)) ($(env))") |
| 261 | + push!(keybindings, only("$n")) |
| 262 | + push!(shown_envs, expanded_env) |
| 263 | + end |
| 264 | + menu = TerminalMenus.RadioMenu(option_list, keybindings=keybindings, pagesize=length(option_list)) |
| 265 | + default = something( |
| 266 | + # select the first non-default env by default, if possible |
| 267 | + findfirst(!=(Base.active_project()), shown_envs), |
| 268 | + 1 |
| 269 | + ) |
| 270 | + print(ctx.io, "\e[1A\e[1G\e[0J") # go up one line, to the start, and clear it |
| 271 | + printstyled(ctx.io, " └ "; color=:green) |
| 272 | + choice = try |
| 273 | + TerminalMenus.request("Select environment:", menu, cursor=default) |
| 274 | + catch err |
| 275 | + if err isa InterruptException # if ^C is entered |
| 276 | + println(ctx.io) |
| 277 | + return false |
| 278 | + end |
| 279 | + rethrow() |
| 280 | + end |
| 281 | + choice == -1 && return false |
| 282 | + API.activate(shown_envs[choice]) do |
| 283 | + API.add(string.(available_pkgs)) |
| 284 | + end |
| 285 | + elseif (lower_resp in ["n"]) |
| 286 | + return false |
| 287 | + else |
| 288 | + println(ctx.io, "Selection not recognized") |
| 289 | + return false |
| 290 | + end |
| 291 | + if length(available_pkgs) < length(pkgs) |
| 292 | + return false # declare that some pkgs couldn't be installed |
| 293 | + else |
| 294 | + return true |
| 295 | + end |
| 296 | +end |
| 297 | + |
| 298 | + |
| 299 | + |
| 300 | +function __init__() |
| 301 | + if isdefined(Base, :active_repl) |
| 302 | + repl_init(Base.active_repl) |
| 303 | + else |
| 304 | + atreplinit() do repl |
| 305 | + if isinteractive() && repl isa REPL.LineEditREPL |
| 306 | + isdefined(repl, :interface) || (repl.interface = REPL.setup_interface(repl)) |
| 307 | + repl_init(repl) |
| 308 | + end |
| 309 | + end |
| 310 | + end |
| 311 | + push!(empty!(REPL.install_packages_hooks), try_prompt_pkg_add) |
| 312 | +end |
| 313 | + |
| 314 | +include("precompile.jl") |
| 315 | + |
| 316 | +end |
0 commit comments