Skip to content

Commit 5104543

Browse files
author
KristofferC
committed
make the REPL specific code into an extension
Co-authored-by: Mark Kittisopikult
1 parent 12d2de1 commit 5104543

File tree

13 files changed

+648
-431
lines changed

13 files changed

+648
-431
lines changed

Project.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ version = "1.11.0"
99
Artifacts = "56f22d72-fd6d-98f1-02f0-08ddc0907c33"
1010
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
1111
Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
12+
Example = "7876af07-990d-54b4-ab0e-23690620f79a"
1213
FileWatching = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee"
1314
LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433"
1415
Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
1516
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
1617
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
1718
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
18-
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
1919
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
2020
SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce"
2121
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
@@ -24,6 +24,12 @@ Tar = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e"
2424
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
2525
p7zip_jll = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0"
2626

27+
[weakdeps]
28+
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
29+
30+
[extensions]
31+
REPLExt = "REPL"
32+
2733
[compat]
2834
HistoricalStdlibVersions = "1.2"
2935

ext/REPLExt/REPLExt.jl

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
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
10+
using .Pkg: Types, Operations, API, Registry, Resolve, REPLMode
11+
12+
using .REPLMode: Statement, CommandSpec, Command, prepare_cmd
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+
181+
function try_prompt_pkg_add(pkgs::Vector{Symbol})
182+
ctx = try
183+
Context()
184+
catch
185+
# Context() will error if there isn't an active project.
186+
# If we can't even do that, exit early.
187+
return false
188+
end
189+
if isempty(ctx.registries)
190+
if !REG_WARNED[]
191+
printstyled(ctx.io, ""; color=:green)
192+
printstyled(ctx.io, "Attempted to find missing packages in package registries but no registries are installed.\n")
193+
printstyled(ctx.io, ""; color=:green)
194+
printstyled(ctx.io, "Use package mode to install a registry. `pkg> registry add` will install the default registries.\n\n")
195+
REG_WARNED[] = true
196+
end
197+
return false
198+
end
199+
available_uuids = [Types.registered_uuids(ctx.registries, String(pkg)) for pkg in pkgs] # vector of vectors
200+
filter!(u -> all(!isequal(Operations.JULIA_UUID), u), available_uuids) # "julia" is in General but not installable
201+
isempty(available_uuids) && return false
202+
available_pkgs = pkgs[isempty.(available_uuids) .== false]
203+
isempty(available_pkgs) && return false
204+
resp = try
205+
plural1 = length(pkgs) == 1 ? "" : "s"
206+
plural2 = length(available_pkgs) == 1 ? "a package" : "packages"
207+
plural3 = length(available_pkgs) == 1 ? "is" : "are"
208+
plural4 = length(available_pkgs) == 1 ? "" : "s"
209+
missing_pkg_list = length(pkgs) == 1 ? String(pkgs[1]) : "[$(join(pkgs, ", "))]"
210+
available_pkg_list = length(available_pkgs) == 1 ? String(available_pkgs[1]) : "[$(join(available_pkgs, ", "))]"
211+
msg1 = "Package$(plural1) $(missing_pkg_list) not found, but $(plural2) named $(available_pkg_list) $(plural3) available from a registry."
212+
for line in linewrap(msg1, io = ctx.io, padding = length(""))
213+
printstyled(ctx.io, ""; color=:green)
214+
println(ctx.io, line)
215+
end
216+
printstyled(ctx.io, ""; color=:green)
217+
println(ctx.io, "Install package$(plural4)?")
218+
msg2 = string("add ", join(available_pkgs, ' '))
219+
for (i, line) in pairs(linewrap(msg2; io = ctx.io, padding = length(string(" | ", REPLMode.promptf()))))
220+
printstyled(ctx.io, ""; color=:green)
221+
if i == 1
222+
printstyled(ctx.io, REPLMode.promptf(); color=:blue)
223+
else
224+
print(ctx.io, " "^length(REPLMode.promptf()))
225+
end
226+
println(ctx.io, line)
227+
end
228+
printstyled(ctx.io, ""; color=:green)
229+
Base.prompt(stdin, ctx.io, "(y/n/o)", default = "y")
230+
catch err
231+
if err isa InterruptException # if ^C is entered
232+
println(ctx.io)
233+
return false
234+
end
235+
rethrow()
236+
end
237+
if isnothing(resp) # if ^D is entered
238+
println(ctx.io)
239+
return false
240+
end
241+
resp = strip(resp)
242+
lower_resp = lowercase(resp)
243+
if lower_resp in ["y", "yes"]
244+
API.add(string.(available_pkgs))
245+
elseif lower_resp in ["o"]
246+
editable_envs = filter(v -> v != "@stdlib", LOAD_PATH)
247+
option_list = String[]
248+
keybindings = Char[]
249+
shown_envs = String[]
250+
# We use digits 1-9 as keybindings in the env selection menu
251+
# That's why we can display at most 9 items in the menu
252+
for i in 1:min(length(editable_envs), 9)
253+
env = editable_envs[i]
254+
expanded_env = Base.load_path_expand(env)
255+
256+
isnothing(expanded_env) && continue
257+
258+
n = length(option_list) + 1
259+
push!(option_list, "$(n): $(pathrepr(expanded_env)) ($(env))")
260+
push!(keybindings, only("$n"))
261+
push!(shown_envs, expanded_env)
262+
end
263+
menu = TerminalMenus.RadioMenu(option_list, keybindings=keybindings, pagesize=length(option_list))
264+
default = something(
265+
# select the first non-default env by default, if possible
266+
findfirst(!=(Base.active_project()), shown_envs),
267+
1
268+
)
269+
print(ctx.io, "\e[1A\e[1G\e[0J") # go up one line, to the start, and clear it
270+
printstyled(ctx.io, ""; color=:green)
271+
choice = try
272+
TerminalMenus.request("Select environment:", menu, cursor=default)
273+
catch err
274+
if err isa InterruptException # if ^C is entered
275+
println(ctx.io)
276+
return false
277+
end
278+
rethrow()
279+
end
280+
choice == -1 && return false
281+
API.activate(shown_envs[choice]) do
282+
API.add(string.(available_pkgs))
283+
end
284+
elseif (lower_resp in ["n"])
285+
return false
286+
else
287+
println(ctx.io, "Selection not recognized")
288+
return false
289+
end
290+
if length(available_pkgs) < length(pkgs)
291+
return false # declare that some pkgs couldn't be installed
292+
else
293+
return true
294+
end
295+
end
296+
297+
298+
299+
function __init__()
300+
if isdefined(Base, :active_repl)
301+
repl_init(Base.active_repl)
302+
else
303+
atreplinit() do repl
304+
if isinteractive() && repl isa REPL.LineEditREPL
305+
isdefined(repl, :interface) || (repl.interface = REPL.setup_interface(repl))
306+
repl_init(repl)
307+
end
308+
end
309+
end
310+
push!(empty!(REPL.install_packages_hooks), try_prompt_pkg_add)
311+
end
312+
313+
include("precompile.jl")
314+
315+
end

0 commit comments

Comments
 (0)