Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
version:
- 'min'
- '1'
- 'pre'
- 'nightly'
R:
- 'release'
Expand Down
126 changes: 91 additions & 35 deletions src/RPrompt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,17 @@ import ..RCall:
libR,
rparse_p,
reval_p,
reval,
findNamespace,
getNamespace,
rcall,
rlang,
rlang_p,
rcall_p,
rprint,
rcopy,
render,
sexp,
protect,
unprotect,
prepare_inline_julia_code,
Expand All @@ -22,13 +28,14 @@ import ..RCall:
REvalError,
RParseEOF


function simple_showerror(io::IO, er)
Base.with_output_color(:red, io) do io
print(io, "ERROR: ")
showerror(io, er)
println(io)
end

return nothing
end

function parse_status(script::String)
Expand All @@ -42,7 +49,7 @@ function parse_status(script::String)
status = :error
end
end
status
return status
end

function repl_eval(script::String, stdout::IO, stderr::IO)
Expand Down Expand Up @@ -73,6 +80,12 @@ function repl_eval(script::String, stdout::IO, stderr::IO)
end
end

@static if isdefined(LineEdit, :check_show_hints)
refresh_line_(s) = LineEdit.check_show_hint(s)
else
refresh_line_(s) = LineEdit.refresh_line(s)
end

function bracketed_paste_callback(s, o...)
input = LineEdit.bracketed_paste(s)
sbuffer = LineEdit.buffer(s)
Expand All @@ -99,7 +112,7 @@ function bracketed_paste_callback(s, o...)
# parse the input line by line
while nextpos < m
next_result = findnext("\n", input, nextpos + 1)
if next_result == nothing
if isnothing(next_result)
nextpos = m
else
nextpos = next_result[1]
Expand All @@ -111,7 +124,7 @@ function bracketed_paste_callback(s, o...)
(nextpos == m && !endswith(input, '\n'))
# error / continue and the end / at the end but no new line
LineEdit.replace_line(s, input[oldpos:end])
LineEdit.refresh_line(s)
refresh_line_(s)
break
elseif status == :incomplete && nextpos < m
continue
Expand All @@ -129,44 +142,83 @@ function bracketed_paste_callback(s, o...)
end
oldpos = nextpos + 1
end
LineEdit.refresh_line(s)
refresh_line_(s)
end

mutable struct RCompletionProvider <: LineEdit.CompletionProvider
r::REPL.LineEditREPL
struct RCompletionProvider <: LineEdit.CompletionProvider
repl::REPL.LineEditREPL
line_modify_lock::ReentrantLock
hint_generation_lock::ReentrantLock
function RCompletionProvider(repl::REPL.LineEditREPL)
repl.mistate = @something(repl.mistate, LineEdit.init_state(REPL.terminal(repl), repl.interface))
@static if hasfield(LineEdit.MIState, :hint_generation_lock)
hint_generation_lock = repl.mistate.hint_generation_lock
else
hint_generation_lock = ReentrantLock()
end
@static if hasfield(LineEdit.MIState, :line_modify_lock)
line_modify_lock = repl.mistate.line_modify_lock
else
line_modify_lock = ReentrantLock()
end

return new(repl, hint_generation_lock, line_modify_lock)
end
end

# Julia PR #54311 (backported to 1.11) added the `hint` argument
if v"1.11.0-beta1.46" <= VERSION < v"1.12.0-DEV.0" || VERSION >= v"1.12.0-DEV.468"
@static if v"1.11.0-beta1.46" <= VERSION < v"1.12.0-DEV.0" || VERSION >= v"1.12.0-DEV.468"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is @static needed?

using REPL.REPLCompletions: bslash_completions
else
function bslash_completions(string::String, pos::Int, hint::Bool=false)
return REPLCompletions.bslash_completions(string, pos)
end
end

# Julia PR 54800 messed up REPL completion, fix adapted from https://github.com/JuliaLang/IJulia.jl/pull/1147
if isdefined(REPLCompletions, :named_completion) # julia#54800 (julia 1.12)
completion_text_(c) = REPLCompletions.named_completion(c).completion::String
else
completion_text_(c) = REPLCompletions.completion_text(c)
end

function LineEdit.complete_line(c::RCompletionProvider, s; hint::Bool=false)
buf = s.input_buffer
partial = String(buf.data[1:buf.ptr-1])
# complete latex
full = LineEdit.input_string(s)
ret, range, should_complete = bslash_completions(full, lastindex(partial), hint)[2]
if length(ret) > 0 && should_complete
return map(REPLCompletions.completion_text, ret), partial[range], should_complete
end
reval("library(utils)")
@lock c.hint_generation_lock begin
buf = s.input_buffer
partial = String(take!(copy(buf))) # String(buf.data[1:buf.ptr-1])
# complete latex
full = LineEdit.input_string(s)
ret, range, should_complete = bslash_completions(full, lastindex(partial), hint)[2]
if length(ret) > 0 && should_complete
return map(completion_text_, ret), partial[range], should_complete
end

# complete r
utils = findNamespace("utils")
rcall_p(utils[".assignLinebuffer"], partial)
rcall_p(utils[".assignEnd"], length(partial))
token = rcopy(rcall_p(utils[".guessTokenFromLine"]))
rcall_p(utils[".completeToken"])
ret = rcopy(Array, rcall_p(utils[".retrieveCompletions"]))
if length(ret) > 0
return ret, token, true
end
# complete r
# XXX As of Julia 1.12, this happens on a background thread
# and findNamespace + function pointers seems to be unsafe in that context, so we must
# use the slightly slower explicit language

rcall_p(reval("utils:::.assignLinebuffer"), partial)
rcall_p(reval("utils:::.assignEnd"), length(partial))
token = rcopy(reval("utils:::.guessTokenFromLine()"))
reval("utils:::.completeToken()")
ret = rcopy(Vector{String}, reval("utils:::.retrieveCompletions()"))::Vector{String}

return String[], "", false
# faster way that doesn't seem to play nice with testing on Julia 1.12
# utils = findNamespace("utils")
# rcall_p(utils[".assignLinebuffer"], partial)
# rcall_p(utils[".assignEnd"], length(partial))
# token = rcopy(rcall_p(utils[".guessTokenFromLine"]))
# rcall_p(utils[".completeToken"])
# ret = rcopy(Array, rcall_p(utils[".retrieveCompletions"]))

if length(ret) > 0
return ret, token, true
end

return String[], "", false
end
end

function create_r_repl(repl, main)
Expand Down Expand Up @@ -224,13 +276,16 @@ function create_r_repl(repl, main)
end

function repl_init(repl)
mirepl = isdefined(repl,:mi) ? repl.mi : repl
main_mode = mirepl.interface.modes[1]
r_mode = create_r_repl(mirepl, main_mode)
push!(mirepl.interface.modes,r_mode)
if !isdefined(repl, :interface)
repl.interface = REPL.setup_interface(repl)
end
interface = repl.interface
main_mode = interface.modes[1]
r_mode = create_r_repl(repl, main_mode)
push!(repl.interface.modes,r_mode)

r_prompt_keymap = Dict{Any,Any}(
'$' => function (s,args...)
'$' => function (s, args...)
if isempty(s) || position(LineEdit.buffer(s)) == 0
buf = copy(LineEdit.buffer(s))
LineEdit.transition(s, r_mode) do
Expand All @@ -243,12 +298,13 @@ function repl_init(repl)
)

main_mode.keymap_dict = LineEdit.keymap_merge(main_mode.keymap_dict, r_prompt_keymap);
nothing
return nothing
end

function repl_inited(repl)
mirepl = isdefined(repl,:mi) ? repl.mi : repl
any(:prompt in fieldnames(typeof(m)) && m.prompt == "R> " for m in mirepl.interface.modes)
interface = repl.interface

return any(:prompt in fieldnames(typeof(m)) && m.prompt == "R> " for m in interface.modes)
end

end # module
3 changes: 2 additions & 1 deletion test/namespaces.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ module NamespaceTests
@test rcopy(rcall(MASS.ginv, RObject([1 2; 0 4]))) ≈ [1 -0.5; 0 0.25]
@rimport MASS as mass
@test rcopy(rcall(mass.ginv, RObject([1 2; 0 4]))) ≈ [1 -0.5; 0 0.25]
@rlibrary MASS
# explicit eval necessary as of Julia 1.12 because we're nested under an `if`
@eval(@rlibrary MASS)
@test rcopy(rcall(ginv, RObject([1 2; 0 4]))) ≈ [1 -0.5; 0 0.25]
end

Expand Down
13 changes: 8 additions & 5 deletions test/repl.jl
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,18 @@ Base.link_pipe!(err, reader_supports_async=true, writer_supports_async=true)

repl = REPL.LineEditREPL(FakeTerminal(input.out, output.in, err.in), true)

repltask = @async begin
REPL.run_repl(repl)
end
@info "Starting REPL...."
Threads.@spawn REPL.run_repl(repl)
@info "REPL spawned"

send_repl(x, enter=true) = write(input, enter ? "$x\n" : x)

function read_repl(io::IO, x)
cache = Ref{Any}("")
read_task = @task cache[] = readuntil(io, x)
t = Base.Timer((_) -> Base.throwto(read_task,
ErrorException("Expect \"$x\", but wait too long.")), 5)
schedule(read_task)
t = Base.Timer((_) -> Base.throwto(read_task,
ErrorException("Expect \"$x\", but wait too long.")), 2)
fetch(read_task)
close(t)
cache[]
Expand Down Expand Up @@ -73,12 +73,15 @@ send_repl("\\alp\t", false)
send_repl("\t", false)
@test check_repl_stdout("α")

@info "linebreak"
send_repl("")
@test check_repl_stdout("\n")

@info "foo"
send_repl("foo]")
@test check_repl_stderr("unexpected")

@info "stop"
send_repl("stop('something is wrong')")
@test check_repl_stderr("something is wrong")

Expand Down
53 changes: 27 additions & 26 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ using TestSetExtensions
using DataStructures: OrderedDict
using RCall: RClass

@testset ExtendedTestSet "installation" begin
include("installation.jl")
end
# @testset ExtendedTestSet "installation" begin
# include("installation.jl")
# end

# before RCall does anything
const R_PPSTACKTOP_INITIAL = unsafe_load(cglobal((:R_PPStackTop, RCall.libR), Int))
Expand Down Expand Up @@ -52,37 +52,38 @@ println(R"l10n_info()")
@test rcopy(Vector{String}, reval(".libPaths()")) == libpaths
end

tests = ["basic",
"convert/base",
"convert/missing",
"convert/datetime",
"convert/dataframe",
"convert/categorical",
"convert/formula",
"convert/namedtuple",
"convert/tuple",
# "convert/axisarray",
"macros",
"namespaces",
"repl",
]
# tests = ["basic",
# "convert/base",
# "convert/missing",
# "convert/datetime",
# "convert/dataframe",
# "convert/categorical",
# "convert/formula",
# "convert/namedtuple",
# "convert/tuple",
# # "convert/axisarray",
# "macros",
# "namespaces",
# "repl",
# ]
tests = ["repl"]

for t in tests
@eval @testset $t begin
include(string($t, ".jl"))
end
end

if Sys.islinux()
# the IJulia tests depend on the R graphics device being set up correctly,
# which is non trivial on non-linux headless devices (e.g. CI)
# it also uses the assumed path to Jupyter on unix
@testset "IJulia" begin
include("ijulia.jl")
end
end
# if Sys.islinux()
# # the IJulia tests depend on the R graphics device being set up correctly,
# # which is non trivial on non-linux headless devices (e.g. CI)
# # it also uses the assumed path to Jupyter on unix
# @testset "IJulia" begin
# include("ijulia.jl")
# end
# end

@info "" RCall.conda_provided_r
# @info "" RCall.conda_provided_r

# make sure we're back where we started
@test unsafe_load(cglobal((:R_PPStackTop, RCall.libR), Int)) == R_PPSTACKTOP_INITIAL
Expand Down