Skip to content

Commit dbf114f

Browse files
authored
Merge pull request #3777 from JuliaLang/kc/repl_extension
make the REPL specific code into an extension
2 parents e3edf39 + a49d479 commit dbf114f

File tree

16 files changed

+687
-468
lines changed

16 files changed

+687
-468
lines changed

Project.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
1515
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
1616
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
1717
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
18-
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
1918
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
2019
SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce"
2120
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
@@ -24,6 +23,12 @@ Tar = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e"
2423
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
2524
p7zip_jll = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0"
2625

26+
[weakdeps]
27+
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
28+
29+
[extensions]
30+
REPLExt = "REPL"
31+
2732
[compat]
2833
HistoricalStdlibVersions = "1.2"
2934

@@ -33,4 +38,4 @@ Preferences = "21216c6a-2e73-6563-6e65-726566657250"
3338
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
3439

3540
[targets]
36-
test = ["Test", "Preferences", "HistoricalStdlibVersions"]
41+
test = ["REPL", "Test", "Preferences", "HistoricalStdlibVersions"]

ext/REPLExt/REPLExt.jl

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
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

Comments
 (0)