From 3adadb533c69ffe1b49d2351d5782ec25f22f395 Mon Sep 17 00:00:00 2001 From: AXeonV <2607343351@qq.com> Date: Mon, 22 Jun 2026 11:43:04 +0800 Subject: [PATCH 1/9] refactor julia plugin & add tests for 0801 & 0804 --- TeXmacs/plugins/julia/julia/MoganJulia.jl | 413 ------------------ TeXmacs/plugins/julia/julia/julia.jl | 145 ++++++ TeXmacs/plugins/julia/julia/tmjl/capture.jl | 74 ++++ .../plugins/julia/julia/tmjl/completion.jl | 28 ++ TeXmacs/plugins/julia/julia/tmjl/display.jl | 158 +++++++ TeXmacs/plugins/julia/julia/tmjl/protocol.jl | 49 +++ TeXmacs/plugins/julia/progs/init-julia.scm | 6 +- TeXmacs/tests/0801.scm | 139 ++++++ TeXmacs/tests/0804.scm | 165 +++++++ 9 files changed, 761 insertions(+), 416 deletions(-) delete mode 100644 TeXmacs/plugins/julia/julia/MoganJulia.jl create mode 100644 TeXmacs/plugins/julia/julia/julia.jl create mode 100644 TeXmacs/plugins/julia/julia/tmjl/capture.jl create mode 100644 TeXmacs/plugins/julia/julia/tmjl/completion.jl create mode 100644 TeXmacs/plugins/julia/julia/tmjl/display.jl create mode 100644 TeXmacs/plugins/julia/julia/tmjl/protocol.jl create mode 100644 TeXmacs/tests/0801.scm create mode 100644 TeXmacs/tests/0804.scm diff --git a/TeXmacs/plugins/julia/julia/MoganJulia.jl b/TeXmacs/plugins/julia/julia/MoganJulia.jl deleted file mode 100644 index aeb82d72e8..0000000000 --- a/TeXmacs/plugins/julia/julia/MoganJulia.jl +++ /dev/null @@ -1,413 +0,0 @@ -# -# MoganJulia.jl -# A Mogan plugin for the Julia language -# (c) 2021 Massimiliano Gubinelli -# 2026 Tianyou Liu -# -# This software falls under the GNU general public license version 3 or later. -# It comes WITHOUT ANY WARRANTY WHATSOEVER. For details, see the file LICENSE -# in the root directory or . -# - -#============================================================================== -Useful links: - -* https://github.com/JuliaDocs/ANSIColoredPrinters.jl -* https://github.com/JuliaDocs/Documenter.jl/pull/1441 -* https://docs.julialang.org/en/v1/stdlib/REPL/ -* https://github.com/JuliaLang/julia/blob/28e63dc65a3ca3850ac2b530bd8a7a89cda551dc/base/version.jl -* https://juliagraphics.github.io/Luxor.jl/stable/moreexamples/ -* https://github.com/JuliaLang/julia/blob/master/stdlib/REPL/docs/src/index.md - -* https://github.com/JuliaLang/julia/issues/3744 - -* https://github.com/JuliaLang/IJulia.jl -* https://julia-doc.readthedocs.io/en/latest/ - -==============================================================================# - -module MoganJulia - -import Base.Libc: flush_cstdio -import REPL: helpmode -import REPL.REPLCompletions: completions, completion_text -using REPL -import UUIDs -import Markdown -import Base: AbstractDisplay, display, redisplay, catch_stack, show - -const current_module = Ref{Module}(Main) -const orig_stdout = Ref{IO}(stdout) -const orig_stderr = Ref{IO}(stderr) - -#=============================================================================# -## Mogan protocol - -const DATA_BEGIN = Char(2) -const DATA_END = Char(5) -const DATA_ESCAPE = Char(27) -const DATA_COMMAND = Char(16) -const VERBATIM = "verbatim:" -const SCHEME = "scheme:" -const COMMAND = "command:" -const PROMPT = "prompt#" - -mogan_escape(data) = replace(replace(replace(data, - DATA_ESCAPE => DATA_ESCAPE * DATA_ESCAPE), - DATA_BEGIN => DATA_ESCAPE * DATA_BEGIN), - DATA_END => DATA_ESCAPE * DATA_END) - -# Mogan expects all output to be bracketed in a DATA_BEGIN and DATA_END -# so that it can determines when the plugin ended the interaction -tm_begin() = write(orig_stdout[], DATA_BEGIN, VERBATIM) -tm_end() = begin - write(orig_stdout[],DATA_END) - flush(orig_stdout[]) -end - -tm_out(data) = begin - write(orig_stdout[], mogan_escape(data)) - flush(orig_stdout[]) -end - -tm_out(header, data) = begin - write(orig_stdout[], - DATA_BEGIN, header, mogan_escape(data), DATA_END) - flush(orig_stdout[]) -end - -tm_err(header, data) = begin - write(orig_stderr[], - DATA_BEGIN, header, mogan_escape(data), DATA_END) - flush(orig_stderr[]) -end - -#=============================================================================# -### Flush all redirected streams to Mogan - -function flush_all() - flush_cstdio() # flush writes to stdout/stderr by external C code - flush(stdout) - flush(stderr) -end - -#=============================================================================# -### Stream redirection (from IJulia) - -# create a wrapper type around redirected stdio streams, -# both for overloading things like `flush` and so that we -# can set properties like `color`. -struct TMJuliaStdio{IO_t <: IO} <: Base.AbstractPipe - io::IOContext{IO_t} - read_stream::Base.PipeEndpoint -end - -TMJuliaStdio(io::IO, read_stream::Base.PipeEndpoint, stream::AbstractString="unknown") = - TMJuliaStdio{typeof(io)}(IOContext(io, :color=>false, - :mogan_stream=>stream, - :displaysize=>displaysize()), read_stream) -Base.pipe_reader(io::TMJuliaStdio) = io.io.io -Base.pipe_writer(io::TMJuliaStdio) = io.io.io -Base.lock(io::TMJuliaStdio) = lock(io.io.io) -Base.unlock(io::TMJuliaStdio) = unlock(io.io.io) -Base.in(key_value::Pair, io::TMJuliaStdio) = in(key_value, io.io) -Base.haskey(io::TMJuliaStdio, key) = haskey(io.io, key) -Base.getindex(io::TMJuliaStdio, key) = getindex(io.io, key) -Base.get(io::TMJuliaStdio, key, default) = get(io.io, key, default) -Base.displaysize(io::TMJuliaStdio) = displaysize(io.io) -Base.unwrapcontext(io::TMJuliaStdio) = Base.unwrapcontext(io.io) -Base.setup_stdio(io::TMJuliaStdio, readable::Bool) = Base.setup_stdio(io.io.io, readable) - -Base.flush(io::TMJuliaStdio) = begin - #write(orig_stdout[],"FLUSHING $(get(io.io, :mogan_stream, "error"))\n") - Base.flush(io.io.io) - # add one more char so that we do not block on readavailable later - write(io.io.io,"!") - local buf = chop(String(readavailable(io.read_stream))); - buf == "" && return - if get(io.io, :mogan_stream, "error") == "stdout" - tm_out(buf * "\n") - elseif get(io.io, :mogan_stream, "error") == "stderr" - tm_err(VERBATIM, buf) - end -end - -if VERSION < v"1.7.0-DEV.254" - for s in ("stdout", "stderr", "stdin") - f = Symbol("redirect_", s) - sq = QuoteNode(Symbol(s)) - @eval function Base.$f(io::TMJuliaStdio) - io[:mogan_stream] != $s && throw(ArgumentError(string("expecting ", $s, " stream"))) - Core.eval(Base, Expr(:(=), $sq, io)) - return io - end - end -end - -#=============================================================================# -### display redirection - -# need special handling for showing a string as a textmime -# type, since in that case the string is assumed to be -# raw data unless it is text/plain -israwtext(::MIME, x::AbstractString) = true -israwtext(::MIME"text/plain", x::AbstractString) = false -israwtext(::MIME, x) = false - -# convert x to a string of type mime, making sure to use an -# IOContext that tells the underlying show function to limit output -function limitstringmime(mime::MIME, x) - buf = IOBuffer() - if israwtext(mime, x) - return String(x) - else - show(IOContext(buf, :limit=>true, :color=>false), mime, x) - end - return String(take!(buf)) -end - -struct InlineDisplay <: AbstractDisplay end - -showtofile(file::AbstractString, m::MIME, x) = begin - open("$(ENV["TEXMACS_HOME_PATH"])/system/tmp/$(file)", "w") do io - show(io, m, x) - end - tm_out("file:", file) -end - -sendimage(ext::AbstractString, m::MIME, x) = begin - buf = IOBuffer() - show(buf, m, x) - tm_out("mogan:","|julia-output-$(UUIDs.uuid1()).$(ext)>|0.618par|||>") -end - -display(d::InlineDisplay, m::MIME"image/png", x) = - sendimage("png", m, x) - -display(d::InlineDisplay, m::MIME"image/jpeg", x) = - sendimage("jpg", m, x) - -display(d::InlineDisplay, m::MIME"application/pdf", x) = - sendimage("pdf", m, x) - -display(d::InlineDisplay, m::MIME"text/html", x) = - tm_out("html:", limitstringmime(m, x)) - -display(d::InlineDisplay, m::MIME"text/latex", x) = begin - s = strip(limitstringmime(m, x)) - # 去除外层 $$, $, \[ \], or \( \) 修饰符直接解析 - s = replace(s, r"^(\$\$?|\\\[|\\\()|(\$\$?|\\\]|\\\))$" => "") - s = strip(s) - # 去掉环境编号 - s = replace(s, "begin{equation}" => "begin{equation*}") - s = replace(s, "end{equation}" => "end{equation*}") - s = replace(s, "begin{align}" => "begin{align*}") - s = replace(s, "end{align}" => "end{align*}") - if occursin(r"^\\begin", s) - tm_out("latex:", s) - else - tm_out("latex:", "\$\\rmfamily{" * s * "}\$") - end -end - -display(d::InlineDisplay, m::MIME"text/markdown", x) = - display(d, MIME("text/html"), Markdown.html(x)) - -display(d::InlineDisplay, m::MIME"text/plain", s::AbstractString) = - tm_out(s) - -# fallback -display(d::InlineDisplay, m::MIME, x) = - tm_out(limitstringmime(m, x)) - -# generic display overloading -display(d::InlineDisplay, x::Markdown.MD) = display(d, MIME("text/markdown"), x) - -# we try to display data according to these mime types -# in order -const tm_mimetypes = [ - MIME("image/svg"), - MIME("application/pdf"), - MIME("image/png"), - MIME("image/jpg"), - MIME("text/latex"), - MIME("text/html"), - MIME("text/markdown")] - -is_symbolic_type(t::Type) = begin - t isa Union && return false - s = string(t) - (occursin("SymPy", s) || occursin("Symbolics", s) || occursin("SymbolicUtils", s) || s == "Num") && return true - try - m = parentmodule(t) - m_name = string(Symbol(m)) - return occursin("SymPy", m_name) || occursin("Symbolics", m_name) || occursin("SymbolicUtils", m_name) - catch - return false - end -end - -is_symbolic_object(x) = is_symbolic_type(typeof(x)) -is_symbolic_object(x::AbstractArray) = any(is_symbolic_object, x) -is_symbolic_object(x::Tuple) = any(is_symbolic_object, x) -is_symbolic_object(x::Set) = any(is_symbolic_object, x) -is_symbolic_object(x::Dict) = any(is_symbolic_object, keys(x)) || any(is_symbolic_object, values(x)) - -function display(d::InlineDisplay, x) - if is_symbolic_object(x) - if showable(MIME("text/latex"), x) - display(d, MIME("text/latex"), x) - return - elseif isdefined(Main, :Latexify) - try - lx = Main.Latexify.latexify(x) - display(d, MIME("text/latex"), lx) - return - catch - # If latexify fails, fall back to default behavior - end - end - end - - for m in tm_mimetypes - if showable(m, x) - display(d, m, x) - return - end - end - # default behaviour is showing text - display(d, MIME("text/plain"), x) -# tm_out("TODO: display an object of type [$(typeof(x))]") -end - -#=============================================================================# -### Some utilities - -function banner() - io = IOBuffer() - # the Julia banner is in REPL in Julia >1.11, and in Base before that - # so we need to do a little chasing - if isdefined(REPL,:banner) - REPL.banner(io) - elseif isdefined(Base,:banner) - Base.banner(io) - else - write(io, "Cannot find the startup banner, sorry!\n"); - end - write(io, "Julia plugin for Mogan STEM.\n"); - tm_out(String(take!(io))) -end - -function pdf_out(x) - if showable(MIME("application/pdf"), x) - display(MIME("application/pdf"), x) - else - tm_out("[Cannot display PDF for $(typeof(x))]") - end -end - -function do_tab_complete(cmd::AbstractString) - # syntax [DATA_COMMAND](complete [STRING] [CURSOR]) - try - pos = 12 - arg1,pos = Meta.parse(cmd,pos; greedy=false) # [STRING] - arg2,pos = Meta.parse(cmd,pos; greedy=false) # [CURSOR] - if isa(arg1,AbstractString) && isa(arg2,Integer) - ret,range,shouldcomplete = completions(arg1,arg2) - compls = join(unique!(map(x -> "\"$(completion_text(x)[range.stop+2-range.start:end])\"",ret))," ") - tm_out("scheme:", "(tuple \"$(arg1[range])\" $(compls))") - end - catch e - # ignore errors - end -end - -#=============================================================================# -### Main loop - -# we do not want to exit on SIGINT -# we can then catch InterruptException -Base.exit_on_sigint(false) - -local read_stdout, read_stderr -# redirect output/error -read_stdout, = redirect_stdout() -redirect_stdout(TMJuliaStdio(stdout,read_stdout,"stdout")) -read_stderr, = redirect_stderr() -redirect_stderr(TMJuliaStdio(stderr,read_stderr,"stderr")) -#redirect_stdin(TMJuliaStdio(stdin,"stdin")) - -# redirect display -pushdisplay(InlineDisplay()) - -# print banner -tm_begin() -banner() -tm_out(PROMPT,">>> ") -tm_end() - -# go -n = 0 # execution counter -ans = nothing # record last successful answer in ans - -while true - line = readline(stdin) - length(line) == 0 && continue - if line[1] == DATA_COMMAND - # is tab completion the only possible command? - do_tab_complete(line) - continue - end - lines = [] - while line != "" - push!(lines, line) - line = readline(stdin) - end - local code = join(lines,"\n") - local result = nothing - local err = nothing - global ans = nothing - tm_begin() - try - global n += 1 - # "; ..." cells are interpreted as shell commands for run - code = replace(code, r"^\s*;.*$" => - m -> string(replace(m, r"^\s*;" => "Base.repl_cmd(`"), - "`, stdout)")) - # a cell beginning with "? ..." is interpreted as a help request - hcode = replace(code, r"^\s*\?" => "") - # Let's try to run the input - if hcode != code # help request - buf = IOBuffer() - help = Core.eval(Main, helpmode(buf, hcode)) - #flush_output() - tm_out("HELP: $(String(take!(buf)))\n") - display(help) - else - # finally run the code! - result = include_string(current_module[], code, "In[$n]") - REPL.ends_with_semicolon(code) ? result = nothing : ans = result - end - catch e - err = e - result = catch_stack() - end - - # output - try - if err != nothing - Base.invokelatest(Base.display_error, stderr, result) - elseif result != nothing - Base.invokelatest(display, result) - #show(stdout, result) # display the result as string - end - catch e - write(stdout, "Error showing values $(e)"); - Base.invokelatest(Base.display_error, stderr, catch_stack()) - end - flush_all() # send all to mogan -# flush_output() # send all to mogan - tm_end() -end # while true - -end # module MoganJulia diff --git a/TeXmacs/plugins/julia/julia/julia.jl b/TeXmacs/plugins/julia/julia/julia.jl new file mode 100644 index 0000000000..5b6b1b3c61 --- /dev/null +++ b/TeXmacs/plugins/julia/julia/julia.jl @@ -0,0 +1,145 @@ +# +# MoganJulia.jl +# A Mogan plugin for the Julia language +# (c) 2021 Massimiliano Gubinelli +# 2026 Tianyou Liu +# +# This software falls under the GNU general public license version 3 or later. +# It comes WITHOUT ANY WARRANTY WHATSOEVER. For details, see the file LICENSE +# in the root directory or . +# + +module MoganJulia + +# Imports +using REPL +using Markdown +using UUIDs +import Base: AbstractDisplay, display, redisplay, catch_stack, show +import REPL: helpmode +import REPL.REPLCompletions: completions, completion_text +import Base.Libc: flush_cstdio + +const current_module = Ref{Module}(Main) +const orig_stdout = Ref{IO}(stdout) +const orig_stderr = Ref{IO}(stderr) + +# Resolve include path relative to the directory containing this file +const julia_plugin_dir = @__DIR__ + +# Include submodules/files +include(joinpath(julia_plugin_dir, "tmjl", "protocol.jl")) +include(joinpath(julia_plugin_dir, "tmjl", "capture.jl")) +include(joinpath(julia_plugin_dir, "tmjl", "display.jl")) +include(joinpath(julia_plugin_dir, "tmjl", "completion.jl")) + +#=============================================================================# +### Some utilities + +function banner() + io = IOBuffer() + # the Julia banner is in REPL in Julia >1.11, and in Base before that + # so we need to do a little chasing + if isdefined(REPL,:banner) + REPL.banner(io) + elseif isdefined(Base,:banner) + Base.banner(io) + else + write(io, "Cannot find the startup banner, sorry!\n"); + end + write(io, "Julia plugin for Mogan STEM.\n"); + tm_out(String(take!(io))) +end + +#=============================================================================# +### Main loop + +# we do not want to exit on SIGINT +# we can then catch InterruptException +Base.exit_on_sigint(false) + +local read_stdout, read_stderr +# redirect output/error +read_stdout, = redirect_stdout() +redirect_stdout(TMJuliaStdio(stdout,read_stdout,"stdout")) +read_stderr, = redirect_stderr() +redirect_stderr(TMJuliaStdio(stderr,read_stderr,"stderr")) +#redirect_stdin(TMJuliaStdio(stdin,"stdin")) + +# redirect display +pushdisplay(InlineDisplay()) + +# print banner +tm_begin() +banner() +tm_out(PROMPT,">>> ") +tm_end() + +# go +n = 0 # execution counter +ans = nothing # record last successful answer in ans + +while true + line = readline(stdin) + if length(line) == 0 && eof(stdin) + break + end + length(line) == 0 && continue + if line[1] == DATA_COMMAND + # is tab completion the only possible command? + do_tab_complete(line) + continue + end + lines = [] + while line != "" + push!(lines, line) + line = readline(stdin) + end + local code = join(lines,"\n") + local result = nothing + local err = nothing + global ans = nothing + tm_begin() + try + global n += 1 + # "; ..." cells are interpreted as shell commands for run + code = replace(code, r"^\s*;.*$" => + m -> string(replace(m, r"^\s*;" => "Base.repl_cmd(`"), + "`, stdout)")) + # a cell beginning with "? ..." is interpreted as a help request + hcode = replace(code, r"^\s*\?" => "") + # Let's try to run the input + if hcode != code # help request + buf = IOBuffer() + help = Core.eval(Main, helpmode(buf, hcode)) + #flush_output() + tm_out("HELP: $(String(take!(buf)))\n") + display(help) + else + # finally run the code! + result = include_string(current_module[], code, "In[$n]") + REPL.ends_with_semicolon(code) ? result = nothing : ans = result + end + catch e + err = e + result = catch_stack() + end + + # output + try + if err != nothing + Base.invokelatest(Base.display_error, stderr, result) + elseif result != nothing + Base.invokelatest(display, result) + #show(stdout, result) # display the result as string + end + catch e + write(stdout, "Error showing values $(e)"); + Base.invokelatest(Base.display_error, stderr, catch_stack()) + end + flush_all() # send all to mogan +# flush_output() # send all to mogan + tm_end() +end # while true + +end # module MoganJulia diff --git a/TeXmacs/plugins/julia/julia/tmjl/capture.jl b/TeXmacs/plugins/julia/julia/tmjl/capture.jl new file mode 100644 index 0000000000..6dffaa2751 --- /dev/null +++ b/TeXmacs/plugins/julia/julia/tmjl/capture.jl @@ -0,0 +1,74 @@ +# +# capture.jl +# Standard stream capturing and redirection for Mogan Julia plugin +# (c) 2021 Massimiliano Gubinelli +# 2026 Tianyou Liu +# +# This software falls under the GNU general public license version 3 or later. +# It comes WITHOUT ANY WARRANTY WHATSOEVER. For details, see the file LICENSE +# in the root directory or . +# + +import Base.Libc: flush_cstdio + +#=============================================================================# +### Flush all redirected streams to Mogan + +function flush_all() + flush_cstdio() # flush writes to stdout/stderr by external C code + flush(stdout) + flush(stderr) +end + +#=============================================================================# +### Stream redirection (from IJulia) + +# create a wrapper type around redirected stdio streams, +# both for overloading things like `flush` and so that we +# can set properties like `color`. +struct TMJuliaStdio{IO_t <: IO} <: Base.AbstractPipe + io::IOContext{IO_t} + read_stream::Base.PipeEndpoint +end + +TMJuliaStdio(io::IO, read_stream::Base.PipeEndpoint, stream::AbstractString="unknown") = + TMJuliaStdio{typeof(io)}(IOContext(io, :color=>false, + :mogan_stream=>stream, + :displaysize=>displaysize()), read_stream) +Base.pipe_reader(io::TMJuliaStdio) = io.io.io +Base.pipe_writer(io::TMJuliaStdio) = io.io.io +Base.lock(io::TMJuliaStdio) = lock(io.io.io) +Base.unlock(io::TMJuliaStdio) = unlock(io.io.io) +Base.in(key_value::Pair, io::TMJuliaStdio) = in(key_value, io.io) +Base.haskey(io::TMJuliaStdio, key) = haskey(io.io, key) +Base.getindex(io::TMJuliaStdio, key) = getindex(io.io, key) +Base.get(io::TMJuliaStdio, key, default) = get(io.io, key, default) +Base.displaysize(io::TMJuliaStdio) = displaysize(io.io) +Base.unwrapcontext(io::TMJuliaStdio) = Base.unwrapcontext(io.io) +Base.setup_stdio(io::TMJuliaStdio, readable::Bool) = Base.setup_stdio(io.io.io, readable) + +Base.flush(io::TMJuliaStdio) = begin + #write(orig_stdout[],"FLUSHING $(get(io.io, :mogan_stream, "error"))\n") + Base.flush(io.io.io) + # add one more char so that we do not block on readavailable later + write(io.io.io,"!") + local buf = chop(String(readavailable(io.read_stream))); + buf == "" && return + if get(io.io, :mogan_stream, "error") == "stdout" + tm_out(buf * "\n") + elseif get(io.io, :mogan_stream, "error") == "stderr" + tm_err(VERBATIM, buf) + end +end + +if VERSION < v"1.7.0-DEV.254" + for s in ("stdout", "stderr", "stdin") + f = Symbol("redirect_", s) + sq = QuoteNode(Symbol(s)) + @eval function Base.$f(io::TMJuliaStdio) + io[:mogan_stream] != $s && throw(ArgumentError(string("expecting ", $s, " stream"))) + Core.eval(Base, Expr(:(=), $sq, io)) + return io + end + end +end diff --git a/TeXmacs/plugins/julia/julia/tmjl/completion.jl b/TeXmacs/plugins/julia/julia/tmjl/completion.jl new file mode 100644 index 0000000000..63480f8a32 --- /dev/null +++ b/TeXmacs/plugins/julia/julia/tmjl/completion.jl @@ -0,0 +1,28 @@ +# +# completion.jl +# Autocompletion protocol implementation for Mogan Julia plugin +# (c) 2021 Massimiliano Gubinelli +# 2026 Tianyou Liu +# +# This software falls under the GNU general public license version 3 or later. +# It comes WITHOUT ANY WARRANTY WHATSOEVER. For details, see the file LICENSE +# in the root directory or . +# + +import REPL.REPLCompletions: completions, completion_text + +function do_tab_complete(cmd::AbstractString) + # syntax [DATA_COMMAND](complete [STRING] [CURSOR]) + try + pos = 12 + arg1,pos = Meta.parse(cmd,pos; greedy=false) # [STRING] + arg2,pos = Meta.parse(cmd,pos; greedy=false) # [CURSOR] + if isa(arg1,AbstractString) && isa(arg2,Integer) + ret,range,shouldcomplete = completions(arg1,arg2) + compls = join(unique!(map(x -> "\"$(completion_text(x)[range.stop+2-range.start:end])\"",ret))," ") + tm_out("scheme:", "(tuple \"$(arg1[range])\" $(compls))") + end + catch e + # ignore errors + end +end diff --git a/TeXmacs/plugins/julia/julia/tmjl/display.jl b/TeXmacs/plugins/julia/julia/tmjl/display.jl new file mode 100644 index 0000000000..6d5ad4c172 --- /dev/null +++ b/TeXmacs/plugins/julia/julia/tmjl/display.jl @@ -0,0 +1,158 @@ +# +# display.jl +# Inline rich displays and symbolic math formatting for Mogan Julia plugin +# (c) 2021 Massimiliano Gubinelli +# 2026 Tianyou Liu +# +# This software falls under the GNU general public license version 3 or later. +# It comes WITHOUT ANY WARRANTY WHATSOEVER. For details, see the file LICENSE +# in the root directory or . +# + +import UUIDs +import Markdown +import Base: AbstractDisplay, display, redisplay, catch_stack, show + +#=============================================================================# +### display redirection + +# need special handling for showing a string as a textmime +# type, since in that case the string is assumed to be +# raw data unless it is text/plain +israwtext(::MIME, x::AbstractString) = true +israwtext(::MIME"text/plain", x::AbstractString) = false +israwtext(::MIME, x) = false + +# convert x to a string of type mime, making sure to use an +# IOContext that tells the underlying show function to limit output +function limitstringmime(mime::MIME, x) + buf = IOBuffer() + if israwtext(mime, x) + return String(x) + else + show(IOContext(buf, :limit=>true, :color=>false), mime, x) + end + return String(take!(buf)) +end + +struct InlineDisplay <: AbstractDisplay end + +showtofile(file::AbstractString, m::MIME, x) = begin + open("$(ENV["TEXMACS_HOME_PATH"])/system/tmp/$(file)", "w") do io + show(io, m, x) + end + tm_out("file:", file) +end + +sendimage(ext::AbstractString, m::MIME, x) = begin + buf = IOBuffer() + show(buf, m, x) + tm_out("mogan:","|julia-output-$(UUIDs.uuid1()).$(ext)>|0.618par|||>") +end + +display(d::InlineDisplay, m::MIME"image/png", x) = + sendimage("png", m, x) + +display(d::InlineDisplay, m::MIME"image/jpeg", x) = + sendimage("jpg", m, x) + +display(d::InlineDisplay, m::MIME"application/pdf", x) = + sendimage("pdf", m, x) + +display(d::InlineDisplay, m::MIME"text/html", x) = + tm_out("html:", limitstringmime(m, x)) + +display(d::InlineDisplay, m::MIME"text/latex", x) = begin + s = strip(limitstringmime(m, x)) + # 去除外层 $$, $, \[ \], or \( \) 修饰符直接解析 + s = replace(s, r"^(\$\$?|\\\[|\\\()|(\$\$?|\\\]|\\\))$" => "") + s = strip(s) + # 去掉环境编号 + s = replace(s, "begin{equation}" => "begin{equation*}") + s = replace(s, "end{equation}" => "end{equation*}") + s = replace(s, "begin{align}" => "begin{align*}") + s = replace(s, "end{align}" => "end{align*}") + if occursin(r"^\\begin", s) + tm_out("latex:", s) + else + tm_out("latex:", "\$\\rmfamily{" * s * "}\$") + end +end + +display(d::InlineDisplay, m::MIME"text/markdown", x) = + display(d, MIME("text/html"), Markdown.html(x)) + +display(d::InlineDisplay, m::MIME"text/plain", s::AbstractString) = + tm_out(s) + +# fallback +display(d::InlineDisplay, m::MIME, x) = + tm_out(limitstringmime(m, x)) + +# generic display overloading +display(d::InlineDisplay, x::Markdown.MD) = display(d, MIME("text/markdown"), x) + +# we try to display data according to these mime types +# in order +const tm_mimetypes = [ + MIME("image/svg"), + MIME("application/pdf"), + MIME("image/png"), + MIME("image/jpg"), + MIME("text/latex"), + MIME("text/html"), + MIME("text/markdown")] + +is_symbolic_type(t::Type) = begin + t isa Union && return false + s = string(t) + (occursin("SymPy", s) || occursin("Symbolics", s) || occursin("SymbolicUtils", s) || s == "Num") && return true + try + m = parentmodule(t) + m_name = string(Symbol(m)) + return occursin("SymPy", m_name) || occursin("Symbolics", m_name) || occursin("SymbolicUtils", m_name) + catch + return false + end +end + +is_symbolic_object(x) = is_symbolic_type(typeof(x)) +is_symbolic_object(x::AbstractArray) = any(is_symbolic_object, x) +is_symbolic_object(x::Tuple) = any(is_symbolic_object, x) +is_symbolic_object(x::Set) = any(is_symbolic_object, x) +is_symbolic_object(x::Dict) = any(is_symbolic_object, keys(x)) || any(is_symbolic_object, values(x)) + +function display(d::InlineDisplay, x) + if is_symbolic_object(x) + if showable(MIME("text/latex"), x) + display(d, MIME("text/latex"), x) + return + elseif isdefined(Main, :Latexify) + try + lx = Main.Latexify.latexify(x) + display(d, MIME("text/latex"), lx) + return + catch + # If latexify fails, fall back to default behavior + end + end + end + + for m in tm_mimetypes + if showable(m, x) + display(d, m, x) + return + end + end + # default behaviour is showing text + display(d, MIME("text/plain"), x) +# tm_out("TODO: display an object of type [$(typeof(x))]") +end + +function pdf_out(x) + if showable(MIME("application/pdf"), x) + display(MIME("application/pdf"), x) + else + tm_out("[Cannot display PDF for $(typeof(x))]") + end +end diff --git a/TeXmacs/plugins/julia/julia/tmjl/protocol.jl b/TeXmacs/plugins/julia/julia/tmjl/protocol.jl new file mode 100644 index 0000000000..3b44cfad73 --- /dev/null +++ b/TeXmacs/plugins/julia/julia/tmjl/protocol.jl @@ -0,0 +1,49 @@ +# +# protocol.jl +# Mogan/TeXmacs plugin protocol implementation for Julia +# (c) 2021 Massimiliano Gubinelli +# 2026 Tianyou Liu +# +# This software falls under the GNU general public license version 3 or later. +# It comes WITHOUT ANY WARRANTY WHATSOEVER. For details, see the file LICENSE +# in the root directory or . +# + +const DATA_BEGIN = Char(2) +const DATA_END = Char(5) +const DATA_ESCAPE = Char(27) +const DATA_COMMAND = Char(16) +const VERBATIM = "verbatim:" +const SCHEME = "scheme:" +const COMMAND = "command:" +const PROMPT = "prompt#" + +mogan_escape(data) = replace(replace(replace(data, + DATA_ESCAPE => DATA_ESCAPE * DATA_ESCAPE), + DATA_BEGIN => DATA_ESCAPE * DATA_BEGIN), + DATA_END => DATA_ESCAPE * DATA_END) + +# Mogan expects all output to be bracketed in a DATA_BEGIN and DATA_END +# so that it can determines when the plugin ended the interaction +tm_begin() = write(orig_stdout[], DATA_BEGIN, VERBATIM) +tm_end() = begin + write(orig_stdout[], DATA_END) + flush(orig_stdout[]) +end + +tm_out(data) = begin + write(orig_stdout[], mogan_escape(data)) + flush(orig_stdout[]) +end + +tm_out(header, data) = begin + write(orig_stdout[], + DATA_BEGIN, header, mogan_escape(data), DATA_END) + flush(orig_stdout[]) +end + +tm_err(header, data) = begin + write(orig_stderr[], + DATA_BEGIN, header, mogan_escape(data), DATA_END) + flush(orig_stderr[]) +end diff --git a/TeXmacs/plugins/julia/progs/init-julia.scm b/TeXmacs/plugins/julia/progs/init-julia.scm index 885d705bdc..4f5034801c 100644 --- a/TeXmacs/plugins/julia/progs/init-julia.scm +++ b/TeXmacs/plugins/julia/progs/init-julia.scm @@ -20,9 +20,9 @@ (define (julia-entry) (url->system (string->url - (if (url-exists? "$TEXMACS_HOME_PATH/plugins/julia/julia/MoganJulia.jl") - "$TEXMACS_HOME_PATH/plugins/julia/julia/MoganJulia.jl" - "$TEXMACS_PATH/plugins/julia/julia/MoganJulia.jl")))) + (if (url-exists? "$TEXMACS_HOME_PATH/plugins/julia/julia/julia.jl") + "$TEXMACS_HOME_PATH/plugins/julia/julia/julia.jl" + "$TEXMACS_PATH/plugins/julia/julia/julia.jl")))) (define (julia-launcher) (let* ((boot (string-quote (julia-entry)))) diff --git a/TeXmacs/tests/0801.scm b/TeXmacs/tests/0801.scm new file mode 100644 index 0000000000..c1faa2be5e --- /dev/null +++ b/TeXmacs/tests/0801.scm @@ -0,0 +1,139 @@ +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; MODULE : 0801.scm +;; DESCRIPTION : Tests for Julia plugin integration and session execution +;; COPYRIGHT : (C) 2026 Mogan STEM +;; +;; This software falls under the GNU general public license version 3 or later. +;; It comes WITHOUT ANY WARRANTY WHATSOEVER. For details, see the file LICENSE +;; in the root directory or . +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(import (scheme base) + (scheme file) + (liii check) + (liii os) + (liii string)) + +(load (string-append (getenv "TEXMACS_PATH") "/plugins/julia/progs/init-julia.scm")) +(load (string-append (getenv "TEXMACS_PATH") "/plugins/julia/progs/code/julia-lang.scm")) + +(check-set-mode! 'report-failed) + +;; Helper function to read a physical file into a string +(define (read-physical-file filepath) + (let ((port (open-input-file filepath))) + (let loop ((chars '()) (c (read-char port))) + (if (eof-object? c) + (begin + (close-input-port port) + (list->string (reverse chars))) + (loop (cons c chars) (read-char port)))))) + +;; Helper to check if a physical file exists +(define (physical-file-exists? filepath) + (catch #t + (lambda () + (let ((port (open-input-file filepath))) + (close-input-port port) + #t)) + (lambda args #f))) + +;; Helper to remove a physical file +(define (physical-remove filepath) + (catch #t + (lambda () + (remove filepath) + #t) + (lambda args #f))) + +;; Helper to resolve platform-specific absolute system path +(define (get-system-path relative-path) + (url->system (system->url (string-append (getenv "TEXMACS_PATH") relative-path)))) + +;; Helper to execute commands cross-platform using the correct system shell +(define (run-shell-command cmd) + (if (os-windows?) + (os-call (string-append "cmd.exe /c \"" cmd "\"")) + (os-call (string-append "/bin/sh -c '" cmd "'")))) + +;; Helper to write lines physically using cross-platform shell echo +(define (write-physical-input filepath lines first?) + (unless (null? lines) + (let ((op (if first? " > " " >> "))) + (if (os-windows?) + (os-call (string-append "cmd.exe /c \"echo " (car lines) op filepath "\"")) + (os-call (string-append "/bin/sh -c 'echo \"" (car lines) "\"" op filepath "'"))) + (write-physical-input filepath (cdr lines) #f)))) + +;; 1. Scheme-side unit tests +(define (test-julia-scheme-side) + (check (julia-serialize "julia" "1 + 2") => "1 + 2\n\n") + (check (list? (parser-feature "julia" "keyword")) => #t) + (check (list? (parser-feature "julia" "operator")) => #t) +) + +;; 2. Session execution integration tests +(define (test-julia-session) + (if (url-exists-in-path? "julia") + (let* ((temp-dir (os-temp-dir)) + (input-path (url->system (system->url (string-append temp-dir "/0801_input.txt")))) + (output-path (url->system (system->url (string-append temp-dir "/0801_output.txt")))) + (julia-script (get-system-path "/plugins/julia/julia/julia.jl")) + (input-lines (list "1 + 2" + "" + "? readdir" + "" + "[1.0 1.0; 1.0 1.0]" + "" + "sqrt(-1.0)" + "" + "nonexistent_variable_0801" + "" + ";echo \\\"hello from shell\\\"" + "" + "using Markdown" + "Markdown.parse(\\\"hello **world**\\\")" + ""))) + + ;; Clean up old files if they exist + (when (physical-file-exists? input-path) (physical-remove input-path)) + (when (physical-file-exists? output-path) (physical-remove output-path)) + + ;; Write input commands physically using shell echo helper + (write-physical-input input-path input-lines #t) + + ;; Execute the julia session via cross-platform shell command + (let* ((julia-bin (if (os-windows?) "julia" "env -u LD_LIBRARY_PATH julia")) + (cmd (string-append julia-bin " --startup-file=no " julia-script " < " input-path " > " output-path " 2>&1"))) + (run-shell-command cmd)) + + ;; Read the output file physically + (let ((output (read-physical-file output-path))) + ; (display "ACTUAL SESSION OUTPUT (0801): ") (display output) (newline) + ;; Assertions based on "How to Test" in 0801.md + (check (string-contains? output "verbatim:3") => #t) + (check (string-contains? output "verbatim:HELP:") => #t) + (check (string-contains? output "readdir") => #t) + (check (string-contains? output "Matrix{Float64}") => #t) + (check (string-contains? output "DomainError") => #t) + (check (string-contains? output "UndefVarError") => #t) + (check (string-contains? output "hello from shell") => #t) + (check (string-contains? output "hello world") => #t) + ) ;let + + ;; Clean up + (physical-remove input-path) + (physical-remove output-path) + ) ;let + (display "Skipping physical Julia session tests because Julia is not in the path.\n") + ) ;if +) ;define + +;;; ========== 测试入口 ========== + +(tm-define (test_0801) + (test-julia-scheme-side) + (test-julia-session) + (check-report)) diff --git a/TeXmacs/tests/0804.scm b/TeXmacs/tests/0804.scm new file mode 100644 index 0000000000..f61b27b71e --- /dev/null +++ b/TeXmacs/tests/0804.scm @@ -0,0 +1,165 @@ +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; MODULE : 0804.scm +;; DESCRIPTION : Tests for Julia symbolic calculations and LaTeX formatting +;; COPYRIGHT : (C) 2026 Mogan STEM +;; +;; This software falls under the GNU general public license version 3 or later. +;; It comes WITHOUT ANY WARRANTY WHATSOEVER. For details, see the file LICENSE +;; in the root directory or . +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(import (scheme base) + (scheme file) + (liii check) + (liii os) + (liii string)) + +(load (string-append (getenv "TEXMACS_PATH") "/plugins/julia/progs/init-julia.scm")) + +(check-set-mode! 'report-failed) + +;; Helper function to read a physical file into a string +(define (read-physical-file filepath) + (let ((port (open-input-file filepath))) + (let loop ((chars '()) (c (read-char port))) + (if (eof-object? c) + (begin + (close-input-port port) + (list->string (reverse chars))) + (loop (cons c chars) (read-char port)))))) + +;; Helper to check if a physical file exists +(define (physical-file-exists? filepath) + (catch #t + (lambda () + (let ((port (open-input-file filepath))) + (close-input-port port) + #t)) + (lambda args #f))) + +;; Helper to remove a physical file +(define (physical-remove filepath) + (catch #t + (lambda () + (remove filepath) + #t) + (lambda args #f))) + +;; Helper to resolve platform-specific absolute system path +(define (get-system-path relative-path) + (url->system (system->url (string-append (getenv "TEXMACS_PATH") relative-path)))) + +;; Helper to execute commands cross-platform using the correct system shell +(define (run-shell-command cmd) + (if (os-windows?) + (os-call (string-append "cmd.exe /c \"" cmd "\"")) + (os-call (string-append "/bin/sh -c '" cmd "'")))) + +;; Helper to write lines physically using cross-platform shell echo +(define (write-physical-input filepath lines first?) + (unless (null? lines) + (let ((op (if first? " > " " >> "))) + (if (os-windows?) + (os-call (string-append "cmd.exe /c \"echo " (car lines) op filepath "\"")) + (os-call (string-append "/bin/sh -c 'echo \"" (car lines) "\"" op filepath "'"))) + (write-physical-input filepath (cdr lines) #f)))) + +;; Check if Symbolics Julia packages are available +(define (julia-packages-available?) + (and (url-exists-in-path? "julia") + (== (run-shell-command (if (os-windows?) + "julia --startup-file=no -e \"using Symbolics, Latexify, LaTeXStrings\"" + "env -u LD_LIBRARY_PATH julia --startup-file=no -e \"using Symbolics, Latexify, LaTeXStrings\"")) 0))) + +;; Session execution symbolic math tests +(define (test-julia-symbolics) + (if (julia-packages-available?) + (let* ((temp-dir (os-temp-dir)) + (input-path (url->system (system->url (string-append temp-dir "/0804_input.txt")))) + (output-path (url->system (system->url (string-append temp-dir "/0804_output.txt")))) + (julia-script (get-system-path "/plugins/julia/julia/julia.jl")) + (input-lines (list "using Symbolics, Latexify, LaTeXStrings" + "" + "@variables x" + "x^2" + "" + "@variables a b c" + "formula = (a + b)^2 / c" + "" + "@variables a b" + "[a^2, b]" + "" + "@variables a b" + "(a^2, b)" + "" + "@variables a b c d" + "M = [a b; c d]" + "" + "@variables a b" + "Dict(a => b)" + "" + "L\\\"\\\\\\\\int_{0}^{\\\\\\\\infty} e^{-x^2} dx = \\\\\\\\frac{\\\\\\\\sqrt{\\\\\\\\pi}}{2}\\\"" + "" + "@variables x" + "D = Differential(x)" + "expand_derivatives(D(sin(x) * x^2))" + "" + "@variables x y" + "expr = (x^2 - y^2) / (x - y)" + "simplify(expr)" + "" + "@variables x y" + "expr = (x + y)^2" + "expand(expr)" + "" + "1 + 2" + "" + "[1, 2, 3]" + ""))) + + ;; Clean up old files if they exist + (when (physical-file-exists? input-path) (physical-remove input-path)) + (when (physical-file-exists? output-path) (physical-remove output-path)) + + ;; Write input commands physically using shell echo helper + (write-physical-input input-path input-lines #t) + + ;; Execute the julia session via cross-platform shell command + (let* ((julia-bin (if (os-windows?) "julia" "env -u LD_LIBRARY_PATH julia")) + (cmd (string-append julia-bin " --startup-file=no " julia-script " < " input-path " > " output-path " 2>&1"))) + (run-shell-command cmd)) + + ;; Read the output file physically + (let ((output (read-physical-file output-path))) + ; (display "ACTUAL SESSION OUTPUT (0804): ") (display output) (newline) + ;; Assertions based on "How to Test" in 0804.md + (check (string-contains? output "latex:\\begin{equation*}") => #t) + (check (string-contains? output "x^{2}") => #t) + (check (string-contains? output "\\frac{\\left( a + b \\right)^{2}}{c}") => #t) + (check (string-contains? output "a^{2} \\\\") => #t) + (check (string-contains? output "b \\\\") => #t) + (check (string-contains? output "a & b \\\\") => #t) + (check (string-contains? output "c & d \\\\") => #t) + (check (string-contains? output "latex:$\\rmfamily{\\int_{0}^{\\infty} e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}}$") => #t) + (check (string-contains? output "2 ~ x ~ \\sin\\left( x \\right)") => #t) + (check (string-contains? output "x + y") => #t) + (check (string-contains? output "x^{2} + 2 ~ x ~ y + y^{2}") => #t) + (check (string-contains? output "verbatim:3") => #t) + (check (string-contains? output "3-element Vector{Int64}:") => #t) + ) ;let + + ;; Clean up + (physical-remove input-path) + (physical-remove output-path) + ) ;let + (display "Skipping physical Julia symbolic tests because Julia or required packages (Symbolics, Latexify, LaTeXStrings) are not installed.\n") + ) ;if +) ;define + +;;; ========== 测试入口 ========== + +(tm-define (test_0804) + (test-julia-symbolics) + (check-report)) From 6e3e8cfd29d38391da0c5e95e607a080d783a4a0 Mon Sep 17 00:00:00 2001 From: AXeonV <2607343351@qq.com> Date: Mon, 22 Jun 2026 13:07:37 +0800 Subject: [PATCH 2/9] wip --- TeXmacs/tests/0801.scm | 39 ++++++++++++++++++++++++++++++--------- TeXmacs/tests/0804.scm | 39 ++++++++++++++++++++++++++++++--------- 2 files changed, 60 insertions(+), 18 deletions(-) diff --git a/TeXmacs/tests/0801.scm b/TeXmacs/tests/0801.scm index c1faa2be5e..e635bc0505 100644 --- a/TeXmacs/tests/0801.scm +++ b/TeXmacs/tests/0801.scm @@ -55,17 +55,38 @@ ;; Helper to execute commands cross-platform using the correct system shell (define (run-shell-command cmd) (if (os-windows?) - (os-call (string-append "cmd.exe /c \"" cmd "\"")) + (os-call cmd) (os-call (string-append "/bin/sh -c '" cmd "'")))) +(define (shell-echo-unescape line) + (let loop ((chars (string->list line)) (result '())) + (if (null? chars) + (list->string (reverse result)) + (let ((c (car chars))) + (if (and (char=? c #\\) (not (null? (cdr chars)))) + (let ((next (cadr chars))) + (if (or (char=? next #\\) (char=? next #\")) + (loop (cddr chars) (cons next result)) + (loop (cdr chars) (cons c result)))) + (loop (cdr chars) (cons c result))))))) + +(define (bash-echo-content line) + (shell-echo-unescape (shell-echo-unescape line))) + ;; Helper to write lines physically using cross-platform shell echo (define (write-physical-input filepath lines first?) - (unless (null? lines) - (let ((op (if first? " > " " >> "))) - (if (os-windows?) - (os-call (string-append "cmd.exe /c \"echo " (car lines) op filepath "\"")) - (os-call (string-append "/bin/sh -c 'echo \"" (car lines) "\"" op filepath "'"))) - (write-physical-input filepath (cdr lines) #f)))) + (if (os-windows?) + (let ((port (open-output-file filepath))) + (let loop ((lines lines)) + (unless (null? lines) + (display (bash-echo-content (car lines)) port) + (display "\n" port) + (loop (cdr lines)))) + (close-output-port port)) + (unless (null? lines) + (let ((op (if first? " > " " >> "))) + (os-call (string-append "/bin/sh -c 'echo \"" (car lines) "\"" op filepath "'")) + (write-physical-input filepath (cdr lines) #f))))) ;; 1. Scheme-side unit tests (define (test-julia-scheme-side) @@ -76,7 +97,7 @@ ;; 2. Session execution integration tests (define (test-julia-session) - (if (url-exists-in-path? "julia") + (if (supports-julia?) (let* ((temp-dir (os-temp-dir)) (input-path (url->system (system->url (string-append temp-dir "/0801_input.txt")))) (output-path (url->system (system->url (string-append temp-dir "/0801_output.txt")))) @@ -127,7 +148,7 @@ (physical-remove input-path) (physical-remove output-path) ) ;let - (display "Skipping physical Julia session tests because Julia is not in the path.\n") + (display "Skipping physical Julia session tests because Julia is not supported.\n") ) ;if ) ;define diff --git a/TeXmacs/tests/0804.scm b/TeXmacs/tests/0804.scm index f61b27b71e..a683662a88 100644 --- a/TeXmacs/tests/0804.scm +++ b/TeXmacs/tests/0804.scm @@ -54,21 +54,42 @@ ;; Helper to execute commands cross-platform using the correct system shell (define (run-shell-command cmd) (if (os-windows?) - (os-call (string-append "cmd.exe /c \"" cmd "\"")) + (os-call cmd) (os-call (string-append "/bin/sh -c '" cmd "'")))) +(define (shell-echo-unescape line) + (let loop ((chars (string->list line)) (result '())) + (if (null? chars) + (list->string (reverse result)) + (let ((c (car chars))) + (if (and (char=? c #\\) (not (null? (cdr chars)))) + (let ((next (cadr chars))) + (if (or (char=? next #\\) (char=? next #\")) + (loop (cddr chars) (cons next result)) + (loop (cdr chars) (cons c result)))) + (loop (cdr chars) (cons c result))))))) + +(define (bash-echo-content line) + (shell-echo-unescape (shell-echo-unescape line))) + ;; Helper to write lines physically using cross-platform shell echo (define (write-physical-input filepath lines first?) - (unless (null? lines) - (let ((op (if first? " > " " >> "))) - (if (os-windows?) - (os-call (string-append "cmd.exe /c \"echo " (car lines) op filepath "\"")) - (os-call (string-append "/bin/sh -c 'echo \"" (car lines) "\"" op filepath "'"))) - (write-physical-input filepath (cdr lines) #f)))) + (if (os-windows?) + (let ((port (open-output-file filepath))) + (let loop ((lines lines)) + (unless (null? lines) + (display (bash-echo-content (car lines)) port) + (display "\n" port) + (loop (cdr lines)))) + (close-output-port port)) + (unless (null? lines) + (let ((op (if first? " > " " >> "))) + (os-call (string-append "/bin/sh -c 'echo \"" (car lines) "\"" op filepath "'")) + (write-physical-input filepath (cdr lines) #f))))) ;; Check if Symbolics Julia packages are available (define (julia-packages-available?) - (and (url-exists-in-path? "julia") + (and (supports-julia?) (== (run-shell-command (if (os-windows?) "julia --startup-file=no -e \"using Symbolics, Latexify, LaTeXStrings\"" "env -u LD_LIBRARY_PATH julia --startup-file=no -e \"using Symbolics, Latexify, LaTeXStrings\"")) 0))) @@ -154,7 +175,7 @@ (physical-remove input-path) (physical-remove output-path) ) ;let - (display "Skipping physical Julia symbolic tests because Julia or required packages (Symbolics, Latexify, LaTeXStrings) are not installed.\n") + (display "Skipping physical Julia symbolic tests because Julia is not supported or required packages (Symbolics, Latexify, LaTeXStrings) are not installed.\n") ) ;if ) ;define From 25c467c6a4e9a81af81d3476282c5311c29c6894 Mon Sep 17 00:00:00 2001 From: AXeonV <2607343351@qq.com> Date: Mon, 22 Jun 2026 13:25:49 +0800 Subject: [PATCH 3/9] wip --- TeXmacs/tests/0801.scm | 45 ++++++++++++------------------------------ TeXmacs/tests/0804.scm | 45 ++++++++++++------------------------------ 2 files changed, 26 insertions(+), 64 deletions(-) diff --git a/TeXmacs/tests/0801.scm b/TeXmacs/tests/0801.scm index e635bc0505..4543d6e820 100644 --- a/TeXmacs/tests/0801.scm +++ b/TeXmacs/tests/0801.scm @@ -58,35 +58,17 @@ (os-call cmd) (os-call (string-append "/bin/sh -c '" cmd "'")))) -(define (shell-echo-unescape line) - (let loop ((chars (string->list line)) (result '())) - (if (null? chars) - (list->string (reverse result)) - (let ((c (car chars))) - (if (and (char=? c #\\) (not (null? (cdr chars)))) - (let ((next (cadr chars))) - (if (or (char=? next #\\) (char=? next #\")) - (loop (cddr chars) (cons next result)) - (loop (cdr chars) (cons c result)))) - (loop (cdr chars) (cons c result))))))) - -(define (bash-echo-content line) - (shell-echo-unescape (shell-echo-unescape line))) - -;; Helper to write lines physically using cross-platform shell echo +;; Helper to write lines physically using Scheme IO (define (write-physical-input filepath lines first?) - (if (os-windows?) - (let ((port (open-output-file filepath))) - (let loop ((lines lines)) - (unless (null? lines) - (display (bash-echo-content (car lines)) port) - (display "\n" port) - (loop (cdr lines)))) - (close-output-port port)) - (unless (null? lines) - (let ((op (if first? " > " " >> "))) - (os-call (string-append "/bin/sh -c 'echo \"" (car lines) "\"" op filepath "'")) - (write-physical-input filepath (cdr lines) #f))))) + (if first? + (when (physical-file-exists? filepath) (physical-remove filepath))) + (let ((port (open-output-file filepath))) + (let loop ((rest lines)) + (unless (null? rest) + (display (car rest) port) + (newline port) + (loop (cdr rest)))) + (close-output-port port))) ;; 1. Scheme-side unit tests (define (test-julia-scheme-side) @@ -112,17 +94,17 @@ "" "nonexistent_variable_0801" "" - ";echo \\\"hello from shell\\\"" + ";echo \"hello from shell\"" "" "using Markdown" - "Markdown.parse(\\\"hello **world**\\\")" + "Markdown.parse(\"hello **world**\")" ""))) ;; Clean up old files if they exist (when (physical-file-exists? input-path) (physical-remove input-path)) (when (physical-file-exists? output-path) (physical-remove output-path)) - ;; Write input commands physically using shell echo helper + ;; Write input commands physically (write-physical-input input-path input-lines #t) ;; Execute the julia session via cross-platform shell command @@ -132,7 +114,6 @@ ;; Read the output file physically (let ((output (read-physical-file output-path))) - ; (display "ACTUAL SESSION OUTPUT (0801): ") (display output) (newline) ;; Assertions based on "How to Test" in 0801.md (check (string-contains? output "verbatim:3") => #t) (check (string-contains? output "verbatim:HELP:") => #t) diff --git a/TeXmacs/tests/0804.scm b/TeXmacs/tests/0804.scm index a683662a88..9ac65826d1 100644 --- a/TeXmacs/tests/0804.scm +++ b/TeXmacs/tests/0804.scm @@ -57,35 +57,17 @@ (os-call cmd) (os-call (string-append "/bin/sh -c '" cmd "'")))) -(define (shell-echo-unescape line) - (let loop ((chars (string->list line)) (result '())) - (if (null? chars) - (list->string (reverse result)) - (let ((c (car chars))) - (if (and (char=? c #\\) (not (null? (cdr chars)))) - (let ((next (cadr chars))) - (if (or (char=? next #\\) (char=? next #\")) - (loop (cddr chars) (cons next result)) - (loop (cdr chars) (cons c result)))) - (loop (cdr chars) (cons c result))))))) - -(define (bash-echo-content line) - (shell-echo-unescape (shell-echo-unescape line))) - -;; Helper to write lines physically using cross-platform shell echo +;; Helper to write lines physically using Scheme IO (define (write-physical-input filepath lines first?) - (if (os-windows?) - (let ((port (open-output-file filepath))) - (let loop ((lines lines)) - (unless (null? lines) - (display (bash-echo-content (car lines)) port) - (display "\n" port) - (loop (cdr lines)))) - (close-output-port port)) - (unless (null? lines) - (let ((op (if first? " > " " >> "))) - (os-call (string-append "/bin/sh -c 'echo \"" (car lines) "\"" op filepath "'")) - (write-physical-input filepath (cdr lines) #f))))) + (if first? + (when (physical-file-exists? filepath) (physical-remove filepath))) + (let ((port (open-output-file filepath))) + (let loop ((rest lines)) + (unless (null? rest) + (display (car rest) port) + (newline port) + (loop (cdr rest)))) + (close-output-port port))) ;; Check if Symbolics Julia packages are available (define (julia-packages-available?) @@ -121,7 +103,7 @@ "@variables a b" "Dict(a => b)" "" - "L\\\"\\\\\\\\int_{0}^{\\\\\\\\infty} e^{-x^2} dx = \\\\\\\\frac{\\\\\\\\sqrt{\\\\\\\\pi}}{2}\\\"" + "L\"\\int_{0}^{\\infty} e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}\"" "" "@variables x" "D = Differential(x)" @@ -144,7 +126,7 @@ (when (physical-file-exists? input-path) (physical-remove input-path)) (when (physical-file-exists? output-path) (physical-remove output-path)) - ;; Write input commands physically using shell echo helper + ;; Write input commands physically (write-physical-input input-path input-lines #t) ;; Execute the julia session via cross-platform shell command @@ -154,7 +136,6 @@ ;; Read the output file physically (let ((output (read-physical-file output-path))) - ; (display "ACTUAL SESSION OUTPUT (0804): ") (display output) (newline) ;; Assertions based on "How to Test" in 0804.md (check (string-contains? output "latex:\\begin{equation*}") => #t) (check (string-contains? output "x^{2}") => #t) @@ -175,7 +156,7 @@ (physical-remove input-path) (physical-remove output-path) ) ;let - (display "Skipping physical Julia symbolic tests because Julia is not supported or required packages (Symbolics, Latexify, LaTeXStrings) are not installed.\n") + (display "Skipping physical Julia symbolic tests because Julia or required packages are not supported.\n") ) ;if ) ;define From 88e3213fa4598c3a8672879ad117bdbc55145dc4 Mon Sep 17 00:00:00 2001 From: AXeonV <2607343351@qq.com> Date: Mon, 22 Jun 2026 13:47:30 +0800 Subject: [PATCH 4/9] wip --- TeXmacs/tests/0801.scm | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/TeXmacs/tests/0801.scm b/TeXmacs/tests/0801.scm index 4543d6e820..a84037b643 100644 --- a/TeXmacs/tests/0801.scm +++ b/TeXmacs/tests/0801.scm @@ -86,18 +86,18 @@ (julia-script (get-system-path "/plugins/julia/julia/julia.jl")) (input-lines (list "1 + 2" "" - "? readdir" + "?sin" "" - "[1.0 1.0; 1.0 1.0]" + "sin(fill(1.0, (2,2)))" "" "sqrt(-1.0)" "" - "nonexistent_variable_0801" + "non_existent_variable" "" - ";echo \"hello from shell\"" + "readdir()" "" "using Markdown" - "Markdown.parse(\"hello **world**\")" + "Markdown.parse(\"# Title\nthis is **bold** font.\n\nthis is a list:\n- A\n - B\")" ""))) ;; Clean up old files if they exist @@ -117,12 +117,14 @@ ;; Assertions based on "How to Test" in 0801.md (check (string-contains? output "verbatim:3") => #t) (check (string-contains? output "verbatim:HELP:") => #t) - (check (string-contains? output "readdir") => #t) - (check (string-contains? output "Matrix{Float64}") => #t) + (check (string-contains? output "2×2 Matrix{Float64}:") => #t) (check (string-contains? output "DomainError") => #t) (check (string-contains? output "UndefVarError") => #t) - (check (string-contains? output "hello from shell") => #t) - (check (string-contains? output "hello world") => #t) + (check (string-contains? output "Vector{String}:") => #t) + (check (string-contains? output "

Title

") => #t) + (check (string-contains? output "

this is bold font.

") => #t) + (check (string-contains? output "

this is a list:

") => #t) + (check (string-contains? output "
  • A

    ") => #t) ) ;let ;; Clean up From 5bb02759447e035df478aee7766eb3c71d614a03 Mon Sep 17 00:00:00 2001 From: AXeonV <2607343351@qq.com> Date: Mon, 22 Jun 2026 13:56:09 +0800 Subject: [PATCH 5/9] add devel/0805.md --- devel/0805.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 devel/0805.md diff --git a/devel/0805.md b/devel/0805.md new file mode 100644 index 0000000000..369a7d26c7 --- /dev/null +++ b/devel/0805.md @@ -0,0 +1,30 @@ +# [0805] 重构 Julia 代码结构 & 补充单元测试 + +## 1 相关文档 +- [0801.md](0801.md) - 集成 Julia 交互式会话 +- [0804.md](0804.md) - Julia 会话支持符号计算 + +## 2 任务相关的代码文件 +- `TeXmacs/plugins/julia/julia/julia.jl` - 插件入口,主交互循环 (由原 `MoganJulia.jl` 重命名并重构) +- `TeXmacs/plugins/julia/julia/tmjl/protocol.jl` - 核心通信协议实现 +- `TeXmacs/plugins/julia/julia/tmjl/capture.jl` - 标准输入输出流拦截重定向 +- `TeXmacs/plugins/julia/julia/tmjl/display.jl` - 多媒体显示与符号对象自动 LaTeX 格式化 +- `TeXmacs/plugins/julia/julia/tmjl/completion.jl` - Tab 自动补全驱动 +- `TeXmacs/plugins/julia/progs/init-julia.scm` - 更新 Julia 启动入口脚本为 `julia.jl` +- `TeXmacs/tests/0801.scm` - Julia 插件通用端到端集成测试 +- `TeXmacs/tests/0804.scm` - Julia 插件符号计算端到端集成测试 + +## 3 如何测试 + +### 3.1 确定性测试(单元测试) +```bash +xmake r 0801 +xmake r 0804 +xmake b stem +``` + +### 3.2 非确定性测试(文档验证) +此 pr 暂无 + +## 4 What +重构 Julia 代码结构,并补充单元测试。 From 3143732fb983ce54e507be26887bac4cf60ea359 Mon Sep 17 00:00:00 2001 From: AXeonV <2607343351@qq.com> Date: Mon, 22 Jun 2026 14:15:55 +0800 Subject: [PATCH 6/9] fix typo --- TeXmacs/plugins/julia/julia/julia.jl | 2 +- TeXmacs/plugins/julia/julia/tmjl/protocol.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/TeXmacs/plugins/julia/julia/julia.jl b/TeXmacs/plugins/julia/julia/julia.jl index 5b6b1b3c61..fdd86f4c52 100644 --- a/TeXmacs/plugins/julia/julia/julia.jl +++ b/TeXmacs/plugins/julia/julia/julia.jl @@ -1,5 +1,5 @@ # -# MoganJulia.jl +# julia.jl # A Mogan plugin for the Julia language # (c) 2021 Massimiliano Gubinelli # 2026 Tianyou Liu diff --git a/TeXmacs/plugins/julia/julia/tmjl/protocol.jl b/TeXmacs/plugins/julia/julia/tmjl/protocol.jl index 3b44cfad73..472c06133b 100644 --- a/TeXmacs/plugins/julia/julia/tmjl/protocol.jl +++ b/TeXmacs/plugins/julia/julia/tmjl/protocol.jl @@ -24,7 +24,7 @@ mogan_escape(data) = replace(replace(replace(data, DATA_END => DATA_ESCAPE * DATA_END) # Mogan expects all output to be bracketed in a DATA_BEGIN and DATA_END -# so that it can determines when the plugin ended the interaction +# so that it can determine when the plugin ended the interaction tm_begin() = write(orig_stdout[], DATA_BEGIN, VERBATIM) tm_end() = begin write(orig_stdout[], DATA_END) From ec12df70b217bfcbb87c5559e001f480d96046a3 Mon Sep 17 00:00:00 2001 From: AXeonV <2607343351@qq.com> Date: Mon, 22 Jun 2026 16:58:20 +0800 Subject: [PATCH 7/9] add unit tests for Julia backend plugin --- TeXmacs/plugins/julia/tests/runtests.jl | 418 ++++++++++++++++++++++++ devel/0805.md | 17 +- 2 files changed, 429 insertions(+), 6 deletions(-) create mode 100644 TeXmacs/plugins/julia/tests/runtests.jl diff --git a/TeXmacs/plugins/julia/tests/runtests.jl b/TeXmacs/plugins/julia/tests/runtests.jl new file mode 100644 index 0000000000..ad94776e2e --- /dev/null +++ b/TeXmacs/plugins/julia/tests/runtests.jl @@ -0,0 +1,418 @@ +# +# runtests.jl +# Native white-box unit tests for Mogan Julia plugin +# (c) 2026 Tianyou Liu +# +# This software falls under the GNU general public license version 3 or later. +# It comes WITHOUT ANY WARRANTY WHATSOEVER. For details, see the file LICENSE +# in the root directory or . +# + +using Test +using REPL + +# Mock modules/types representing SymPy, Symbolics, SymbolicUtils for test detection +module MockSymPy struct Sym end end +module MockSymbolics struct Num end end +module MockSymbolicUtils struct Basic end end + +# Mock global variables and refs expected by included files +const current_module = Ref{Module}(Main) +const orig_stdout = Ref{IO}(stdout) +const orig_stderr = Ref{IO}(stderr) + +# Resolve path relative to this test file +const julia_plugin_dir = joinpath(@__DIR__, "..", "julia") + +# Include all the modularized Julia plugin source files under test +include(joinpath(julia_plugin_dir, "tmjl", "protocol.jl")) +include(joinpath(julia_plugin_dir, "tmjl", "capture.jl")) +include(joinpath(julia_plugin_dir, "tmjl", "display.jl")) +include(joinpath(julia_plugin_dir, "tmjl", "completion.jl")) + +# A helper module for testing negative symbolic-object detection +module TestSymbolicLike + struct MyNum end +end + +@testset "Julia Backend Unit Tests" begin + + @testset "1. Protocol Escaping (mogan_escape)" begin + # Empty string + @test mogan_escape("") == "" + + # Normal strings with no escape characters + @test mogan_escape("hello world") == "hello world" + @test mogan_escape("NCEA Level 3 Physics") == "NCEA Level 3 Physics" + + # Escaping of individual protocol characters + @test mogan_escape(string(DATA_ESCAPE)) == string(DATA_ESCAPE, DATA_ESCAPE) + @test mogan_escape(string(DATA_BEGIN)) == string(DATA_ESCAPE, DATA_BEGIN) + @test mogan_escape(string(DATA_END)) == string(DATA_ESCAPE, DATA_END) + + # Mixed and consecutive escape characters + @test mogan_escape("a" * DATA_BEGIN * "b" * DATA_END * "c") == + "a" * DATA_ESCAPE * DATA_BEGIN * "b" * DATA_ESCAPE * DATA_END * "c" + + @test mogan_escape(string(DATA_BEGIN, DATA_ESCAPE, DATA_END)) == + string(DATA_ESCAPE, DATA_BEGIN, DATA_ESCAPE, DATA_ESCAPE, DATA_ESCAPE, DATA_END) + end + + @testset "2. Protocol Output Commands" begin + # Buffer capture setup + buf_out = IOBuffer() + buf_err = IOBuffer() + orig_stdout[] = buf_out + orig_stderr[] = buf_err + + # Test tm_begin + tm_begin() + @test String(take!(buf_out)) == string(DATA_BEGIN, VERBATIM) + + # Test tm_end + tm_end() + @test String(take!(buf_out)) == string(DATA_END) + + # Test tm_out (direct string) + tm_out("test-data") + @test String(take!(buf_out)) == "test-data" + + # Test tm_out with header + tm_out("latex:", "x^2") + @test String(take!(buf_out)) == string(DATA_BEGIN, "latex:", "x^2", DATA_END) + + # Test tm_err with header + tm_err("verbatim:", "load-error") + @test String(take!(buf_err)) == string(DATA_BEGIN, "verbatim:", "load-error", DATA_END) + + # Restore original IO streams + orig_stdout[] = stdout + orig_stderr[] = stderr + end + + @testset "3. MIME Display formatting & LaTeX stripping" begin + buf_out = IOBuffer() + orig_stdout[] = buf_out + + # Test text/html display dispatch + display(InlineDisplay(), MIME("text/html"), "

    Julia

    ") + @test String(take!(buf_out)) == string(DATA_BEGIN, "html:", "

    Julia

    ", DATA_END) + + # Test text/plain fallback + display(InlineDisplay(), MIME("text/plain"), "plain text") + @test String(take!(buf_out)) == "plain text" + + # Test text/latex inline stripping of delimiters ($$, $, \[, \], etc.) + display(InlineDisplay(), MIME("text/latex"), "\$\$x^2\$\$") + @test String(take!(buf_out)) == string(DATA_BEGIN, "latex:", "\$\\rmfamily{x^2}\$", DATA_END) + + display(InlineDisplay(), MIME("text/latex"), "\\[ a + b \\]") + @test String(take!(buf_out)) == string(DATA_BEGIN, "latex:", "\$\\rmfamily{a + b}\$", DATA_END) + + display(InlineDisplay(), MIME("text/latex"), "\\( \\sqrt{2} \\)") + @test String(take!(buf_out)) == string(DATA_BEGIN, "latex:", "\$\\rmfamily{\\sqrt{2}}\$", DATA_END) + + # Test LaTeX environments (equation, align) - should rewrite but NOT wrap in \rmfamily + display(InlineDisplay(), MIME("text/latex"), "\\begin{equation}y = x^2\\end{equation}") + @test String(take!(buf_out)) == string(DATA_BEGIN, "latex:", "\\begin{equation*}y = x^2\\end{equation*}", DATA_END) + + display(InlineDisplay(), MIME("text/latex"), "\\begin{align}a &= b\\end{align}") + @test String(take!(buf_out)) == string(DATA_BEGIN, "latex:", "\\begin{align*}a &= b\\end{align*}", DATA_END) + + # Restore original IO streams + orig_stdout[] = stdout + end + + @testset "4. Symbolic Objects Recognition (is_symbolic_object)" begin + # 1. Standard types (should return false) + @test !is_symbolic_object(1) + @test !is_symbolic_object("x") + @test !is_symbolic_object(1.0 + 2.0im) + @test !is_symbolic_object([1, 2, 3]) + @test !is_symbolic_object(Union{Int, Float64}) # Union type edge case + + # 2. Direct symbolic types + @test is_symbolic_object(MockSymPy.Sym()) + @test is_symbolic_object(MockSymbolics.Num()) + @test is_symbolic_object(MockSymbolics.Num()) + + # 3. Deeply nested container checks (Arrays, Tuples, Sets, Dicts) + @test is_symbolic_object([1, MockSymbolics.Num()]) # Array + @test is_symbolic_object((MockSymPy.Sym(), 1.0)) # Tuple + @test is_symbolic_object(Set([MockSymbolics.Num(), 1])) # Set + @test is_symbolic_object(Dict(MockSymPy.Sym() => "value")) # Dict keys + @test is_symbolic_object(Dict("key" => MockSymbolics.Num())) # Dict values + end + + @testset "5. Auto-completion Parsing (do_tab_complete)" begin + buf_out = IOBuffer() + orig_stdout[] = buf_out + + # Test valid command format: DATA_COMMAND (complete "re" 2) + cmd = string(DATA_COMMAND, "(complete \"re\" 2)") + do_tab_complete(cmd) + output = String(take!(buf_out)) + @test occursin("scheme:", output) + @test occursin("tuple", output) + @test occursin("addir", output) || occursin("ad", output) + + # Test invalid completion format (must catch exception internally and not crash) + invalid_cmd = string(DATA_COMMAND, "(complete \"re\" \"invalid_cursor\")") + @test_nowarn do_tab_complete(invalid_cmd) + + # Restore original IO streams + orig_stdout[] = stdout + end + + @testset "6. Stream Redirection (TMJuliaStdio)" begin + old_stdout = stdout + old_stderr = stderr + buf_out = IOBuffer() + buf_err = IOBuffer() + orig_stdout[] = buf_out + orig_stderr[] = buf_err + + # stdout branch + rd_out, wr_out = redirect_stdout() + try + stdio_out = TMJuliaStdio(wr_out, rd_out, "stdout") + println(stdio_out, "stdout-line") + flush(stdio_out) + out_data = String(take!(buf_out)) + @test occursin("stdout-line", out_data) + finally + redirect_stdout(old_stdout) + end + + # stderr branch + rd_err, wr_err = redirect_stderr() + try + stdio_err = TMJuliaStdio(wr_err, rd_err, "stderr") + println(stdio_err, "stderr-line") + flush(stdio_err) + err_data = String(take!(buf_err)) + @test occursin("stderr-line", err_data) + @test occursin(VERBATIM, err_data) + finally + redirect_stderr(old_stderr) + end + + orig_stdout[] = old_stdout + orig_stderr[] = old_stderr + end + + @testset "7. TMJuliaStdio Properties" begin + old_stdout = stdout + rd, wr = redirect_stdout() + try + stdio = TMJuliaStdio(wr, rd, "stdout") + @test get(stdio, :mogan_stream, "") == "stdout" + @test get(stdio, :color, true) == false + finally + redirect_stdout(old_stdout) + end + end + + @testset "8. Code Evaluation Core Logic" begin + # These are the exact primitives used inside the main REPL loop + @test include_string(Main, "1 + 2", "In[1]") == 3 + @test REPL.ends_with_semicolon("1 + 2;") + @test !REPL.ends_with_semicolon("1 + 2") + + # Shell-mode transformation + shell_code = ";pwd" + transformed = replace(shell_code, r"^\s*;.*$" => + m -> string(replace(m, r"^\s*;" => "Base.repl_cmd(`"), + "`, stdout)")) + @test occursin("Base.repl_cmd(`pwd`", transformed) + + # Help-mode transformation + @test replace("?sin", r"^\s*\?" => "") == "sin" + end + + @testset "9. Help Mode Integration" begin + buf_help = IOBuffer() + help_obj = Core.eval(Main, REPL.helpmode(buf_help, "sqrt")) + @test help_obj isa Markdown.MD + end + + @testset "10. Additional MIME Displays" begin + buf_out = IOBuffer() + orig_stdout[] = buf_out + + # Markdown renders through text/html + md = Markdown.parse("# Title\nparagraph") + display(InlineDisplay(), md) + html_out = String(take!(buf_out)) + @test occursin("html:", html_out) + + # A type providing its own text/latex show method + struct LatexType end + Base.show(io::IO, ::MIME"text/latex", ::LatexType) = print(io, "\\frac{1}{2}") + display(InlineDisplay(), LatexType()) + latex_out = String(take!(buf_out)) + @test occursin("latex:", latex_out) + @test occursin("\\frac{1}{2}", latex_out) + + # Fallback text/plain for arbitrary structs + struct PlainType x::Int end + display(InlineDisplay(), PlainType(7)) + plain_out = String(take!(buf_out)) + @test occursin("7", plain_out) + + orig_stdout[] = stdout + end + + @testset "11. LaTeX Display Edge Cases" begin + buf_out = IOBuffer() + orig_stdout[] = buf_out + + # Already-starred environments must not be double-starred + display(InlineDisplay(), MIME("text/latex"), "\\begin{equation*}x = 1\\end{equation*}") + @test String(take!(buf_out)) == string(DATA_BEGIN, "latex:", "\\begin{equation*}x = 1\\end{equation*}", DATA_END) + + # Bracket-style delimiters + display(InlineDisplay(), MIME("text/latex"), " \\[ \\alpha + \\beta \\] ") + @test String(take!(buf_out)) == string(DATA_BEGIN, "latex:", "\$\\rmfamily{\\alpha + \\beta}\$", DATA_END) + + # Empty content + display(InlineDisplay(), MIME("text/latex"), "") + @test String(take!(buf_out)) == string(DATA_BEGIN, "latex:", "\$\\rmfamily{}\$", DATA_END) + + orig_stdout[] = stdout + end + + @testset "12. PDF Output Dispatch" begin + buf_out = IOBuffer() + orig_stdout[] = buf_out + pdf_out(42) + @test String(take!(buf_out)) == "[Cannot display PDF for Int64]" + orig_stdout[] = stdout + end + + @testset "13. Completion Edge Cases" begin + buf_out = IOBuffer() + orig_stdout[] = buf_out + + # Empty prefix at cursor 0 + cmd = string(DATA_COMMAND, "(complete \"\" 0)") + do_tab_complete(cmd) + out1 = String(take!(buf_out)) + @test occursin("scheme:", out1) + + # Module-qualified prefix + cmd = string(DATA_COMMAND, "(complete \"Base.ab\" 7)") + do_tab_complete(cmd) + out2 = String(take!(buf_out)) + @test occursin("scheme:", out2) + + # Malformed commands must not throw + @test_nowarn do_tab_complete(string(DATA_COMMAND, "(complete \"x\")")) + @test_nowarn do_tab_complete(string(DATA_COMMAND, "not-a-form")) + + orig_stdout[] = stdout + end + + @testset "14. Symbolic Object Detection in Containers" begin + @test is_symbolic_object([[MockSymbolics.Num()]]) + @test is_symbolic_object((a=MockSymPy.Sym(), b=1)) + @test is_symbolic_object(Dict(:x => [MockSymbolics.Num(), 1])) + @test !is_symbolic_object(Dict(:x => 1, :y => 2.0)) + @test !is_symbolic_object(Set([1, 2, 3])) + + # Custom type that looks symbolic only by module name + @test !is_symbolic_object(TestSymbolicLike.MyNum()) + end + + @testset "15. Scientific Computing Workflows" begin + using LinearAlgebra + A = [1.0 2.0; 3.0 4.0] + @test det(A) ≈ -2.0 + vals = eigvals(A) + @test length(vals) == 2 + @test prod(vals) ≈ det(A) + + # Display a numeric array through the plugin pipeline + buf_out = IOBuffer() + orig_stdout[] = buf_out + display(InlineDisplay(), A) + arr_out = String(take!(buf_out)) + @test occursin("html:", arr_out) || occursin("1.0", arr_out) + orig_stdout[] = stdout + + # Complex numbers and special values are not symbolic + @test !is_symbolic_object(1 + 2im) + @test !is_symbolic_object(π) + end + + @testset "16. Optional Symbolic Packages Integration" begin + function pkg_available(pkg::String) + try + @eval using $(Symbol(pkg)) + return true + catch + return false + end + end + + if pkg_available("Symbolics") + expr = @eval begin + using Symbolics + @variables x y + x^2 + y^2 + end + @test is_symbolic_object(expr) + buf_out = IOBuffer() + orig_stdout[] = buf_out + display(InlineDisplay(), expr) + sym_out = String(take!(buf_out)) + # Symbolics alone may fall back to text/plain unless text/latex is + # showable or Latexify is loaded. + if showable(MIME("text/latex"), expr) || isdefined(Main, :Latexify) + @test occursin("latex:", sym_out) + else + @test occursin("x^2", sym_out) + end + orig_stdout[] = stdout + else + @test_skip "Symbolics.jl not installed" + end + + if pkg_available("SymPy") + z = @eval begin + using SymPy + SymPy.@syms z + z + end + @test is_symbolic_object(z) + else + @test_skip "SymPy.jl not installed" + end + + if pkg_available("Latexify") + lt = @eval begin + using Latexify + latexify(:(x / y)) + end + buf_out = IOBuffer() + orig_stdout[] = buf_out + display(InlineDisplay(), MIME("text/latex"), lt) + @test occursin("latex:", String(take!(buf_out))) + orig_stdout[] = stdout + else + @test_skip "Latexify.jl not installed" + end + end + + @testset "17. Protocol Escaping Edge Cases" begin + @test mogan_escape("α + β = γ") == "α + β = γ" + @test mogan_escape(string(DATA_ESCAPE, DATA_BEGIN, DATA_END)) == + string(DATA_ESCAPE, DATA_ESCAPE, DATA_ESCAPE, DATA_BEGIN, DATA_ESCAPE, DATA_END) + @test mogan_escape("") == "" + end + + @testset "18. flush_all Utility" begin + @test_nowarn flush_all() + end + +end diff --git a/devel/0805.md b/devel/0805.md index 369a7d26c7..308a1d6020 100644 --- a/devel/0805.md +++ b/devel/0805.md @@ -1,4 +1,4 @@ -# [0805] 重构 Julia 代码结构 & 补充单元测试 +# [0805] 重构 Julia 代码结构 & 补充测试 ## 1 相关文档 - [0801.md](0801.md) - 集成 Julia 交互式会话 @@ -11,20 +11,25 @@ - `TeXmacs/plugins/julia/julia/tmjl/display.jl` - 多媒体显示与符号对象自动 LaTeX 格式化 - `TeXmacs/plugins/julia/julia/tmjl/completion.jl` - Tab 自动补全驱动 - `TeXmacs/plugins/julia/progs/init-julia.scm` - 更新 Julia 启动入口脚本为 `julia.jl` -- `TeXmacs/tests/0801.scm` - Julia 插件通用端到端集成测试 -- `TeXmacs/tests/0804.scm` - Julia 插件符号计算端到端集成测试 +- `TeXmacs/plugins/julia/tests/runtests.jl` - **80个** Julia 插件后端单元测试 +- `TeXmacs/tests/0801.scm` - **13个** Julia 插件通用端到端集成测试 +- `TeXmacs/tests/0804.scm` - **13个** Julia 插件符号计算端到端集成测试 ## 3 如何测试 -### 3.1 确定性测试(单元测试) +### 3.1 确定性测试(集成/单元测试) +集成测试: ```bash xmake r 0801 xmake r 0804 xmake b stem ``` +单元测试(确保环境中安装了 Julia): +```bash +julia TeXmacs/plugins/julia/tests/runtests.jl +``` ### 3.2 非确定性测试(文档验证) -此 pr 暂无 ## 4 What -重构 Julia 代码结构,并补充单元测试。 +重构 Julia 后端插件支持的代码结构,并补充集成/单元测试。 From 58fb9c51721319c4177cf491a646b9896e3e33ae Mon Sep 17 00:00:00 2001 From: AXeonV <2607343351@qq.com> Date: Mon, 22 Jun 2026 19:25:00 +0800 Subject: [PATCH 8/9] add julia ci workflow --- .github/workflows/ci-julia.yml | 48 ++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/workflows/ci-julia.yml diff --git a/.github/workflows/ci-julia.yml b/.github/workflows/ci-julia.yml new file mode 100644 index 0000000000..83ac272764 --- /dev/null +++ b/.github/workflows/ci-julia.yml @@ -0,0 +1,48 @@ +name: Julia Plugin CI + +on: + push: + branches: [main] + paths: + - "TeXmacs/plugins/julia/**" + - ".github/workflows/ci-julia.yml" + pull_request: + branches: [main] + paths: + - "TeXmacs/plugins/julia/**" + - ".github/workflows/ci-julia.yml" + workflow_dispatch: + +jobs: + test-julia: + container: debian:13 + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 1 + + - name: Install system dependencies + run: | + DEBIAN_FRONTEND=noninteractive apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates curl python3 python3-sympy + + - name: Set up Julia + uses: julia-actions/setup-julia@v2 + with: + version: '1.12' + + - name: Cache Julia packages + uses: julia-actions/cache@v2 + + - name: Install Julia packages + env: + PYTHON: /usr/bin/python3 + run: | + python3 -c "import sympy; print('sympy', sympy.__version__)" + julia -e 'using Pkg; Pkg.add(["Symbolics", "Latexify", "LaTeXStrings", "SymPy"])' + + - name: Run Julia unit tests + run: | + julia TeXmacs/plugins/julia/tests/runtests.jl From 26e9c4ac035b48da991e6e6cdf9855f7d1ba0f48 Mon Sep 17 00:00:00 2001 From: AXeonV <2607343351@qq.com> Date: Mon, 22 Jun 2026 19:29:32 +0800 Subject: [PATCH 9/9] add python-dev --- .github/workflows/ci-julia.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-julia.yml b/.github/workflows/ci-julia.yml index 83ac272764..944bf24250 100644 --- a/.github/workflows/ci-julia.yml +++ b/.github/workflows/ci-julia.yml @@ -26,7 +26,7 @@ jobs: - name: Install system dependencies run: | DEBIAN_FRONTEND=noninteractive apt-get update - DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates curl python3 python3-sympy + DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates curl python3 python3-dev python3-sympy - name: Set up Julia uses: julia-actions/setup-julia@v2