Skip to content

Commit e118845

Browse files
authored
Add ability to add output prefixes to the REPL output and use that to implement an IPython mode (#46474)
1 parent 6557542 commit e118845

File tree

5 files changed

+140
-6
lines changed

5 files changed

+140
-6
lines changed

NEWS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ Standard library changes
129129
via the `REPL.activate(::Module)` function or via typing the module in the REPL and pressing
130130
the keybinding Alt-m ([#33872]).
131131

132+
* An "IPython mode" which mimics the behaviour of the prompts and storing the evaluated result in `Out` can be
133+
activated with `REPL.ipython_mode()`. See the manual for how to enable this at startup.
134+
132135
#### SparseArrays
133136

134137
#### Test

stdlib/REPL/docs/src/index.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,20 @@ julia> REPL.activate(CustomMod)
616616
var 8 bytes Int64
617617
```
618618

619+
## IPython mode
620+
621+
It is possible to get an interface which is similar to the IPython REPL with numbered input prompts and output prefixes. This is done by calling `REPL.ipython_mode()`. If you want to have this enabled on startup, add
622+
```julia
623+
atreplinit() do repl
624+
if !isdefined(repl, :interface)
625+
repl.interface = REPL.setup_interface(repl)
626+
end
627+
REPL.ipython_mode(repl)
628+
end
629+
```
630+
631+
to your `startup.jl` file.
632+
619633
## TerminalMenus
620634

621635
TerminalMenus is a submodule of the Julia REPL and enables small, low-profile interactive menus in the terminal.

stdlib/REPL/src/LineEdit.jl

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ mutable struct Prompt <: TextInterface
4949
prompt_prefix::Union{String,Function}
5050
# Same as prefix except after the prompt
5151
prompt_suffix::Union{String,Function}
52+
output_prefix::Union{String,Function}
53+
output_prefix_prefix::Union{String,Function}
54+
output_prefix_suffix::Union{String,Function}
5255
keymap_dict::Dict{Char,Any}
5356
repl::Union{AbstractREPL,Nothing}
5457
complete::CompletionProvider
@@ -1452,7 +1455,6 @@ default_completion_cb(::IOBuffer) = []
14521455
default_enter_cb(_) = true
14531456

14541457
write_prompt(terminal::AbstractTerminal, s::PromptState, color::Bool) = write_prompt(terminal, s.p, color)
1455-
14561458
function write_prompt(terminal::AbstractTerminal, p::Prompt, color::Bool)
14571459
prefix = prompt_string(p.prompt_prefix)
14581460
suffix = prompt_string(p.prompt_suffix)
@@ -1464,6 +1466,17 @@ function write_prompt(terminal::AbstractTerminal, p::Prompt, color::Bool)
14641466
return width
14651467
end
14661468

1469+
function write_output_prefix(io::IO, p::Prompt, color::Bool)
1470+
prefix = prompt_string(p.output_prefix_prefix)
1471+
suffix = prompt_string(p.output_prefix_suffix)
1472+
print(io, prefix)
1473+
color && write(io, Base.text_colors[:bold])
1474+
width = write_prompt(io, p.output_prefix, color)
1475+
color && write(io, Base.text_colors[:normal])
1476+
print(io, suffix)
1477+
return width
1478+
end
1479+
14671480
# On Windows, when launching external processes, we cannot control what assumption they make on the
14681481
# console mode. We thus forcibly reset the console mode at the start of the prompt to ensure they do
14691482
# not leave the console mode in a corrupt state.
@@ -2591,6 +2604,9 @@ function Prompt(prompt
25912604
;
25922605
prompt_prefix = "",
25932606
prompt_suffix = "",
2607+
output_prefix = "",
2608+
output_prefix_prefix = "",
2609+
output_prefix_suffix = "",
25942610
keymap_dict = default_keymap_dict,
25952611
repl = nothing,
25962612
complete = EmptyCompletionProvider(),
@@ -2599,8 +2615,8 @@ function Prompt(prompt
25992615
hist = EmptyHistoryProvider(),
26002616
sticky = false)
26012617

2602-
return Prompt(prompt, prompt_prefix, prompt_suffix, keymap_dict, repl,
2603-
complete, on_enter, on_done, hist, sticky)
2618+
return Prompt(prompt, prompt_prefix, prompt_suffix, output_prefix, output_prefix_prefix, output_prefix_suffix,
2619+
keymap_dict, repl, complete, on_enter, on_done, hist, sticky)
26042620
end
26052621

26062622
run_interface(::Prompt) = nothing

stdlib/REPL/src/REPL.jl

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,11 @@ function display(d::REPLDisplay, mime::MIME"text/plain", x)
260260
x = Ref{Any}(x)
261261
with_repl_linfo(d.repl) do io
262262
io = IOContext(io, :limit => true, :module => active_module(d)::Module)
263+
if d.repl isa LineEditREPL
264+
mistate = d.repl.mistate
265+
mode = LineEdit.mode(mistate)
266+
LineEdit.write_output_prefix(io, mode, get(io, :color, false))
267+
end
263268
get(io, :color, false) && write(io, answer_color(d.repl))
264269
if isdefined(d.repl, :options) && isdefined(d.repl.options, :iocontext)
265270
# this can override the :limit property set initially
@@ -354,8 +359,7 @@ end
354359
355360
consumer is an optional function that takes a REPLBackend as an argument
356361
"""
357-
function run_repl(repl::AbstractREPL, @nospecialize(consumer = x -> nothing); backend_on_current_task::Bool = true)
358-
backend = REPLBackend()
362+
function run_repl(repl::AbstractREPL, @nospecialize(consumer = x -> nothing); backend_on_current_task::Bool = true, backend = REPLBackend())
359363
backend_ref = REPLBackendRef(backend)
360364
cleanup = @task try
361365
destroy(backend_ref, t)
@@ -1062,7 +1066,7 @@ function setup_interface(
10621066

10631067
shell_prompt_len = length(SHELL_PROMPT)
10641068
help_prompt_len = length(HELP_PROMPT)
1065-
jl_prompt_regex = r"^(?:\(.+\) )?julia> "
1069+
jl_prompt_regex = r"^In \[[0-9]+\]: |^(?:\(.+\) )?julia> "
10661070
pkg_prompt_regex = r"^(?:\(.+\) )?pkg> "
10671071

10681072
# Canonicalize user keymap input
@@ -1388,4 +1392,62 @@ function run_frontend(repl::StreamREPL, backend::REPLBackendRef)
13881392
nothing
13891393
end
13901394

1395+
module IPython
1396+
1397+
using ..REPL
1398+
1399+
__current_ast_transforms() = isdefined(Base, :active_repl_backend) ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
1400+
1401+
function repl_eval_counter(hp)
1402+
length(hp.history)-hp.start_idx
1403+
end
1404+
1405+
function out_transform(x, repl=Base.active_repl)
1406+
return quote
1407+
julia_prompt = $repl.interface.modes[1]
1408+
n = $repl_eval_counter(julia_prompt.hist)
1409+
mod = $REPL.active_module()
1410+
if !isdefined(mod, :Out)
1411+
setglobal!(mod, :Out, Dict{Int, Any}())
1412+
end
1413+
local __temp_val = $x # workaround https://github.com/JuliaLang/julia/issues/464511
1414+
if __temp_val !== getglobal(mod, :Out) && __temp_val !== nothing # remove this?
1415+
getglobal(mod, :Out)[n] = __temp_val
1416+
end
1417+
__temp_val
1418+
end
1419+
end
1420+
1421+
function set_prompt(repl=Base.active_repl)
1422+
julia_prompt = repl.interface.modes[1]
1423+
julia_prompt.prompt = () -> string("In [", repl_eval_counter(julia_prompt.hist)+1, "]: ")
1424+
end
1425+
1426+
function set_output_prefix(repl=Base.active_repl)
1427+
julia_prompt = repl.interface.modes[1]
1428+
if REPL.hascolor(repl)
1429+
julia_prompt.output_prefix_prefix = Base.text_colors[:red]
1430+
end
1431+
julia_prompt.output_prefix = () -> string("Out[", repl_eval_counter(julia_prompt.hist), "]: ")
1432+
end
1433+
1434+
function __current_ast_transforms(repltask)
1435+
if repltask === nothing
1436+
isdefined(Base, :active_repl_backend) ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
1437+
else
1438+
repltask.ast_transforms
1439+
end
1440+
end
1441+
1442+
1443+
function ipython_mode(repl=Base.active_repl, repltask=nothing)
1444+
set_prompt(repl)
1445+
set_output_prefix(repl)
1446+
push!(__current_ast_transforms(repltask), ast -> out_transform(ast, repl))
1447+
return
1448+
end
1449+
end
1450+
1451+
import .IPython.ipython_mode
1452+
13911453
end # module

stdlib/REPL/test/repl.jl

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,11 @@ fake_repl() do stdin_write, stdout_read, repl
707707
wait(c)
708708
@test Main.A == 2
709709

710+
# Test removal of prefix in single statement paste
711+
sendrepl2("\e[200~In [12]: A = 2.2\e[201~\n")
712+
wait(c)
713+
@test Main.A == 2.2
714+
710715
# Test removal of prefix in multiple statement paste
711716
sendrepl2("""\e[200~
712717
julia> mutable struct T17599; a::Int; end
@@ -1548,3 +1553,37 @@ fake_repl() do stdin_write, stdout_read, repl
15481553
LineEdit.edit_input(s, input_f)
15491554
@test buffercontents(LineEdit.buffer(s)) == "1234αβ56γ"
15501555
end
1556+
1557+
# Non standard output_prefix, tested via `ipython_mode`
1558+
fake_repl() do stdin_write, stdout_read, repl
1559+
repl.interface = REPL.setup_interface(repl)
1560+
1561+
backend = REPL.REPLBackend()
1562+
repltask = @async begin
1563+
REPL.run_repl(repl; backend)
1564+
end
1565+
1566+
REPL.ipython_mode(repl, backend)
1567+
1568+
global c = Condition()
1569+
sendrepl2(cmd) = write(stdin_write, "$cmd\n notify($(curmod_prefix)c)\n")
1570+
1571+
sendrepl2("\"z\" * \"z\"\n")
1572+
wait(c)
1573+
s = String(readuntil(stdout_read, "\"zz\""; keep=true))
1574+
@test contains(s, "In [1]")
1575+
@test contains(s, "Out[1]: \"zz\"")
1576+
1577+
sendrepl2("\"y\" * \"y\"\n")
1578+
wait(c)
1579+
s = String(readuntil(stdout_read, "\"yy\""; keep=true))
1580+
@test contains(s, "Out[3]: \"yy\"")
1581+
1582+
sendrepl2("Out[1] * Out[3]\n")
1583+
wait(c)
1584+
s = String(readuntil(stdout_read, "\"zzyy\""; keep=true))
1585+
@test contains(s, "Out[5]: \"zzyy\"")
1586+
1587+
write(stdin_write, '\x04')
1588+
Base.wait(repltask)
1589+
end

0 commit comments

Comments
 (0)