diff --git a/Project.toml b/Project.toml index df61805f..42fda173 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Polymake" uuid = "d720cf60-89b5-51f5-aff5-213f193123e7" repo = "https://github.com/oscar-system/Polymake.jl.git" -version = "0.11.23" +version = "0.11.24" [deps] AbstractAlgebra = "c3fe647b-3220-5bb0-a1ea-a7954cac585d" diff --git a/docs/make.jl b/docs/make.jl index 66b575b2..ea836758 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,6 +1,13 @@ using Documenter, Polymake -makedocs(sitename = "Polymake.jl - Documentation", pages = ["index.md", "getting_started.md", "using_polymake_jl.md", "examples.md"]) +makedocs(sitename = "Polymake.jl - Documentation", + pages = [ + "index.md", + "getting_started.md", + "using_polymake_jl.md", + "examples.md", + "shell.md", + ]) deploydocs( repo = "github.com/oscar-system/Polymake.jl.git", diff --git a/docs/src/shell.md b/docs/src/shell.md new file mode 100644 index 00000000..8d7cc14d --- /dev/null +++ b/docs/src/shell.md @@ -0,0 +1,30 @@ +# Polymake REPL + +This section shows how to use the embedded polymake shell. For more details on the polymake shell please see the [polymake documentation](https://polymake.org/doku.php/user_guide/shell). + +To access it type the dollar `$` symbol in an empty line or call `Polymake.prompt()`. The julia prompt should transition to `polymake (common) >`, indicating the currently active polymake application. + +There are a few differences to the proper polymake shell: +- For technical reasons the default application is `common` instead of `polytope`. Thus most calls from other applications should be prefixed with the corresponding application, e.g. `$c = polytope::cube(3)`. The currently active application can also be changed with `application "someapplication";` (this will be indicated in the prompt). +- Incomplete input will be executed (and fail) immediately on pressing return. To enter multi-line input please use `Alt+Enter`. + +## Passing data back and forth + +Objects that are known to polymake can be assigned to and retrieved from the special module `Polymake.Shell`. The variable name in that module corresponds to a polymake shell variable of that name. + +```julia +julia> c = polytope.cube(3); + +julia> Polymake.Shell.cc = c; + +polymake (common) > print $cc->F_VECTOR; +8 12 6 + +julia> Polymake.Shell.cc.H_VECTOR +pm::Vector +1 5 5 1 +``` + +!!! warning + This feature is considered experimental! + There are very little checks on the data being passed around, so please avoid passing temporaries or incompatible objects. diff --git a/src/Polymake.jl b/src/Polymake.jl index accac6bd..58285a4f 100644 --- a/src/Polymake.jl +++ b/src/Polymake.jl @@ -306,10 +306,7 @@ function __init__() set_julia_type("$(name)_OscarNumber", current_type) end - - if isdefined(Base, :active_repl) - run_polymake_repl() - end + try_init_polymake_repl() if isdefined(Main, :IJulia) && Main.IJulia.inited prepare_jupyter_kernel_for_visualization() diff --git a/src/perlobj.jl b/src/perlobj.jl index 6204bc56..429ce453 100644 --- a/src/perlobj.jl +++ b/src/perlobj.jl @@ -76,12 +76,12 @@ function give(obj::BigObject, prop::Union{Symbol,String}) internal_give(obj, prop) end catch ex - ex isa ErrorException && throw(PolymakeError(ex.msg)) + ex isa ErrorException && rethrow(PolymakeError(ex.msg)) if (ex isa InterruptException) @warn """Interrupting polymake is not safe. SIGINT is disabled while waiting for polymake to finish its computations.""" end - rethrow(ex) + rethrow() end return convert_from_property_value(return_obj) end diff --git a/src/repl.jl b/src/repl.jl index 52c92d45..b34ca12b 100644 --- a/src/repl.jl +++ b/src/repl.jl @@ -14,19 +14,28 @@ Base.getproperty(::ShellHelper, name::Symbol) = Polymake.call_function(:User, :g Base.setproperty!(::ShellHelper, name::Symbol, value) = Polymake.call_function(:User, :set_shell_scalar, String(name), value) +const polymakerepl = Ref{LineEdit.Prompt}() struct PolymakeCompletions <: LineEdit.CompletionProvider end -_color(str, magic_number=37) = Base.text_colors[(sum(Int, str) + magic_number) % 0xff] +_color(str, magic_number=69) = Base.text_colors[(sum(Int, str) + magic_number) % 0xff] +_get_app() = Polymake.get_current_app() +_get_prompt_prefix(str = _get_app()) = _color(str) +_get_prompt(app = _get_app()) = "polymake ($app) > " +_get_prompt_suffix() = Base.text_colors[:default] function shell_execute_print(s::String, panel::LineEdit.Prompt) res = convert(Tuple{Bool, String, String, String}, _shell_execute(s)) - panel.prompt=Polymake.get_current_app()*" > " - - panel.prompt_prefix=_color(panel.prompt) + app = _get_app() + panel.prompt=_get_prompt(app) + panel.prompt_prefix=_get_prompt_prefix(app) if res[1] print(Base.stdout, res[2]) + # make sure there is a newline in between if both are nonempty + if !isempty(res[2]) && !isempty(res[3]) && last(res[2]) != '\n' + println() + end print(Base.stderr, res[3]) if !isempty(res[4]) error(res[4]) @@ -35,36 +44,43 @@ function shell_execute_print(s::String, panel::LineEdit.Prompt) if !isempty(res[4]) error(res[4]) else - error("polymake: incomplete statement") + error("polymake: incomplete statement, try Alt+Enter for multi-line input") end end end -function LineEdit.complete_line(c::PolymakeCompletions, s) - partial = REPL.beforecursor(LineEdit.buffer(s)) - full = LineEdit.input_string(s) - res = convert(Tuple{Int, Base.Array{String}}, shell_complete(full)) - offset = first(res) - proposals = res[2] - return proposals, partial[end-offset+1:end], size(proposals,1) > 0 +function LineEdit.complete_line(c::PolymakeCompletions, s; kwargs...) + try + partial = REPL.beforecursor(LineEdit.buffer(s)) + full = LineEdit.input_string(s) + pmres = shell_complete(full) + res = convert(Tuple{Int, Base.Array{String}}, pmres) + offset = first(res) + proposals = res[2] + return proposals, partial[end-offset+1:end], size(proposals,1) > 0 + catch + @debug "error completing polymake line" exception=current_exceptions() + return String[], "", true + end end -function CreatePolymakeREPL(; prompt = Polymake.get_current_app() * " > ", name = :pm, repl = Base.active_repl, main_mode = repl.interface.modes[1]) +function CreatePolymakeREPL(; prompt = _get_prompt(), name = :pm, repl = Base.active_repl, main_mode = repl.interface.modes[1]) mirepl = isdefined(repl,:mi) ? repl.mi : repl # Setup polymake panel panel = LineEdit.Prompt(prompt; # Copy colors from the prompt object - prompt_prefix=_color(prompt), - prompt_suffix=Base.text_colors[:white], - on_enter = REPL.return_callback) + prompt_prefix=_get_prompt_prefix(), + prompt_suffix=_get_prompt_suffix(), + on_enter = (_) -> true, + sticky = true) #on_enter = s->isExpressionComplete(C,push!(copy(LineEdit.buffer(s).data),0))) panel.on_done = REPL.respond(repl,panel; pass_empty = false) do line if !isempty(line) - :(Polymake.shell_execute_print($line, $panel) ) + :( Polymake.shell_execute_print($line, $panel) ) else - :( ) + :( ) end end @@ -86,31 +102,70 @@ function CreatePolymakeREPL(; prompt = Polymake.get_current_app() * " > ", name panel end -global function run_polymake_repl(; - prompt = Polymake.get_current_app() * " > ", +function run_polymake_repl(repl = Base.active_repl; + prompt = _get_prompt(), name = :pm, - key = '$') - repl = Base.active_repl - mirepl = isdefined(repl,:mi) ? repl.mi : repl - # skip repl init if it is not fully loaded - isdefined(mirepl, :interface) && isdefined(mirepl.interface, :modes) || return nothing - main_mode = mirepl.interface.modes[1] - - panel = CreatePolymakeREPL(; prompt=prompt, name=name, repl=repl) - - # Install this mode into the main mode - pm_keymap = Dict{Any,Any}( - key => function (s,args...) - if isempty(s) || position(LineEdit.buffer(s)) == 0 - buf = copy(LineEdit.buffer(s)) - LineEdit.transition(s, panel) do - LineEdit.state(s, panel).input_buffer = buf - end - else - LineEdit.edit_insert(s,key) - end - end - ) - main_mode.keymap_dict = LineEdit.keymap_merge(main_mode.keymap_dict, pm_keymap); - nothing + key = '$', + nothrow = false) + try + repl isa REPL.LineEditREPL || error("only minimal REPL active, cannot add REPL mode, check DEPOT_PATH for stdlib path") + mirepl = isdefined(repl,:mi) ? repl.mi : repl + # skip repl init if it is not fully loaded + isdefined(mirepl, :interface) && isdefined(mirepl.interface, :modes) || return nothing + main_mode = mirepl.interface.modes[1] + + panel = CreatePolymakeREPL(; prompt=prompt, name=name, repl=repl) + global polymakerepl[] = panel + + # Install this mode into the main mode + pm_keymap = Dict{Any,Any}( + key => function (s,args...) + if isempty(s) || position(LineEdit.buffer(s)) == 0 + buf = copy(LineEdit.buffer(s)) + LineEdit.transition(s, panel) do + LineEdit.state(s, panel).input_buffer = buf + end + else + LineEdit.edit_insert(s,key) + end + end + ) + main_mode.keymap_dict = LineEdit.keymap_merge(main_mode.keymap_dict, pm_keymap) + catch ex + if nothrow + # failing to initialize the repl should not be fatal during initialization + @warn ex + else + rethrow() + end + end +end + +function try_init_polymake_repl() + if isdefined(Base, :active_repl) + if Base.active_repl isa REPL.LineEditREPL + run_polymake_repl(nothrow=true) + end + else + atreplinit() do repl + if isinteractive() && repl isa REPL.LineEditREPL + run_polymake_repl(nothrow=true) + end + end + end +end + +function prompt() + if !isassigned(polymakerepl) + run_polymake_repl() + end + mist = Base.active_repl.mistate + pmr = polymakerepl[] + # hide prompt to avoid duplicate prompt printout during transition + pmr.prompt="" + REPL.transition(mist, pmr) do + LineEdit.state(mist, pmr).input_buffer = IOBuffer() + end + pmr.prompt=_get_prompt() + return nothing end