From 5588211a26720e03ece55503decefabc075f7b86 Mon Sep 17 00:00:00 2001 From: c42f Date: Fri, 10 Mar 2023 15:20:18 +1000 Subject: [PATCH 1/3] Rewrite source range `highlight()` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `highlight()` now always prints whole lines of source code, and can highlight arbitrary ranges using box drawing characters, not just with ANSI colors. Use this to fix diagnostic printing so that it's comprehensible in a non color terminal and so that pasting errors into non-color environments works. The box drawing characters from WGL4 seem like a good balance of: * Relatively compatible because they're very old, dating from DOS era: https://en.wikipedia.org/wiki/Box-drawing_character#DOS - note we also use these for formatting log messages from Logging.ConsoleLogger. * Easy to distinguish from the user's source code I've also found it's helpful to prepend any lines of annotation with a Julia # comment where possible - this ensures that copy+paste into websites with syntax highlighting will highlight the annotations separately from the code. A simple example: julia> (x - (c <--- d)) ERROR: ParseError: (x - (c <--- d)) # └──┘ ── invalid operator @ REPL[48]:1:9 Also some semi-related changes included * Generalized/expanded _printstyled() function * Better diagnostic range for try-without-catch --- src/diagnostics.jl | 78 ++++++++----------------- src/parse_stream.jl | 3 +- src/parser.jl | 9 ++- src/source_files.jl | 133 ++++++++++++++++++++++++++++++++++++++++++- src/syntax_tree.jl | 24 ++++---- src/utils.jl | 62 +++++++++++++++++--- test/parser_api.jl | 26 +++++++++ test/runtests.jl | 2 + test/source_files.jl | 96 +++++++++++++++++++++++++++++++ test/syntax_tree.jl | 4 +- test/test_utils.jl | 3 +- test/utils.jl | 17 ++++++ 12 files changed, 377 insertions(+), 80 deletions(-) create mode 100644 test/utils.jl diff --git a/src/diagnostics.jl b/src/diagnostics.jl index ca98b368..31199bbf 100644 --- a/src/diagnostics.jl +++ b/src/diagnostics.jl @@ -40,6 +40,18 @@ end first_byte(d::Diagnostic) = d.first_byte last_byte(d::Diagnostic) = d.last_byte is_error(d::Diagnostic) = d.level == :error +Base.range(d::Diagnostic) = first_byte(d):last_byte(d) + +# Make relative path into a file URL +function _file_url(filename) + @static if Sys.iswindows() + # TODO: Test this with windows terminal + path = join(splitpath(abspath(filename)), '/') + else + path = abspath(filename) + end + "file://$(path)" +end function show_diagnostic(io::IO, diagnostic::Diagnostic, source::SourceFile) color,prefix = diagnostic.level == :error ? (:light_red, "Error") : @@ -49,76 +61,34 @@ function show_diagnostic(io::IO, diagnostic::Diagnostic, source::SourceFile) line, col = source_location(source, first_byte(diagnostic)) linecol = "$line:$col" filename = source.filename + file_href = nothing if !isnothing(filename) locstr = "$filename:$linecol" - if get(io, :color, false) - # Also add hyperlinks in color terminals - url = "file://$(abspath(filename))#$linecol" - locstr = "\e]8;;$url\e\\$locstr\e]8;;\e\\" + if !startswith(filename, "REPL[") + file_href = _file_url(filename)*"#$linecol" end else locstr = "line $linecol" end - print(io, prefix, ": ") - printstyled(io, diagnostic.message, color=color) - printstyled(io, "\n", "@ $locstr", color=:light_black) + _printstyled(io, "# $prefix @ ", fgcolor=:light_black) + _printstyled(io, "$locstr", fgcolor=:light_black, href=file_href) print(io, "\n") - - p = first_byte(diagnostic) - q = last_byte(diagnostic) - text = sourcetext(source) - if q < p || (p == q && source[p] == '\n') - # An empty or invisible range! We expand it symmetrically to make it - # visible. - p = max(firstindex(text), prevind(text, p)) - q = min(lastindex(text), nextind(text, q)) - end - - # p and q mark the start and end of the diagnostic range. For context, - # buffer these out to the surrouding lines. - a,b = source_line_range(source, p, context_lines_before=2, context_lines_after=1) - c,d = source_line_range(source, q, context_lines_before=1, context_lines_after=2) - - hicol = (100,40,40) - - # TODO: show line numbers on left - - print(io, source[a:prevind(text, p)]) - # There's two situations, either - if b >= c - # The diagnostic range is compact and we show the whole thing - # a............... - # .....p...q...... - # ...............b - _printstyled(io, source[p:q]; bgcolor=hicol) - else - # Or large and we trucate the code to show only the region around the - # start and end of the error. - # a............... - # .....p.......... - # ...............b - # (snip) - # c............... - # .....q.......... - # ...............d - _printstyled(io, source[p:b]; bgcolor=hicol) - println(io, "…") - _printstyled(io, source[c:q]; bgcolor=hicol) - end - print(io, source[nextind(text,q):d]) - println(io) + highlight(io, source, range(diagnostic), + note=diagnostic.message, notecolor=color, + context_lines_before=1, context_lines_after=0) end function show_diagnostics(io::IO, diagnostics::AbstractVector{Diagnostic}, source::SourceFile) + first = true for d in diagnostics + first || println(io) + first = false show_diagnostic(io, d, source) end end function show_diagnostics(io::IO, diagnostics::AbstractVector{Diagnostic}, text::AbstractString) - if !isempty(diagnostics) - show_diagnostics(io, diagnostics, SourceFile(text)) - end + show_diagnostics(io, diagnostics, SourceFile(text)) end function emit_diagnostic(diagnostics::AbstractVector{Diagnostic}, diff --git a/src/parse_stream.jl b/src/parse_stream.jl index 2189da23..4157910c 100644 --- a/src/parse_stream.jl +++ b/src/parse_stream.jl @@ -841,7 +841,7 @@ end function emit_diagnostic(stream::ParseStream, mark::ParseStreamPosition; kws...) emit_diagnostic(stream, token_first_byte(stream, mark.token_index), - _next_byte(stream) - 1; kws...) + _next_byte(stream) - 1; kws...) end function emit_diagnostic(stream::ParseStream, mark::ParseStreamPosition, @@ -923,6 +923,7 @@ function validate_tokens(stream::ParseStream) t.orig_kind, t.next_byte) end end + sort!(stream.diagnostics, by=first_byte) end # Tree construction from the list of text ranges held by ParseStream diff --git a/src/parser.jl b/src/parser.jl index 6318054a..9347ee73 100644 --- a/src/parser.jl +++ b/src/parser.jl @@ -2230,6 +2230,7 @@ function parse_try(ps) out_kind = K"try" mark = position(ps) bump(ps, TRIVIA_FLAG) + diagnostic_mark = position(ps) parse_block(ps) has_catch = false has_else = false @@ -2282,12 +2283,16 @@ function parse_try(ps) emit_diagnostic(ps, m, position(ps), warning="`catch` after `finally` will execute out of order") end - if !has_catch && !has_finally + missing_recovery = !has_catch && !has_finally + if missing_recovery # try x end ==> (try (block x) false false false false (error-t)) - bump_invisible(ps, K"error", TRIVIA_FLAG, error="try without catch or finally") + bump_invisible(ps, K"error", TRIVIA_FLAG) end bump_closing_token(ps, K"end") emit(ps, mark, out_kind, flags) + if missing_recovery + emit_diagnostic(ps, diagnostic_mark, error="try without catch or finally") + end end function parse_catch(ps::ParseState) diff --git a/src/source_files.jl b/src/source_files.jl index 66e960bb..b3406315 100644 --- a/src/source_files.jl +++ b/src/source_files.jl @@ -88,7 +88,7 @@ function Base.show(io::IO, ::MIME"text/plain", source::SourceFile) end end -function Base.getindex(source::SourceFile, rng::AbstractRange) +function Base.getindex(source::SourceFile, rng::AbstractUnitRange) i = first(rng) # Convert byte range into unicode String character range. # Assumes valid unicode! (SubString doesn't give us a reliable way to opt @@ -99,7 +99,7 @@ function Base.getindex(source::SourceFile, rng::AbstractRange) end # TODO: Change view() here to `sourcetext` ? -function Base.view(source::SourceFile, rng::AbstractRange) +function Base.view(source::SourceFile, rng::AbstractUnitRange) i = first(rng) j = prevind(source.code, last(rng)+1) SubString(source.code, i, j) @@ -120,3 +120,132 @@ Get the full source text of a `SourceFile` as a string. function sourcetext(source::SourceFile) return source.code end + + +#------------------------------------------------------------------------------- +# Tools for highlighting source ranges + +function _printnote(io, indent, w, note::AbstractString, notecolor) + if isnothing(notecolor) + print(io, note) + else + _printstyled(io, note, fgcolor=notecolor) + end +end +function _printnote(io, indent, w, note::Function, notecolor) + note(io, indent, w) +end + +function _print_marker_line(io, prefix_str, str, underline, singleline, color, + note, notecolor) + # Whitespace equivalent in length to `prefix_str` + # Getting exactly the same width of whitespace as `str` is tricky. + # Especially for mixtures of tabs and spaces. + # tabs are zero width according to textwidth + indent = join(isspace(c) ? c : repeat(' ', textwidth(c)) for c in prefix_str) + + # Assume tabs are 4 wide rather than 0. (fixme: implement tab alignment?) + w = textwidth(str) + 4*count(c->c=='\t', str) + if !isempty(indent) + indent = "#" * (first(indent) == '\t' ? indent : indent[nextind(indent,1):end]) + end + + midchar = '─' + startstr, endstr, singlestart = underline ? ("└","┘","╙") : ("┌","┐","╓") + + markline = + if singleline + w == 0 ? string(indent, startstr) : + w == 1 ? string(indent, singlestart) : + string(indent, startstr, repeat('─', w-2), endstr) + else + if underline && isempty(indent) && w > 1 + string('#', repeat('─', w-2), endstr) + else + s,e = underline ? ("", endstr) : (startstr, "") + w == 0 ? string(indent, s, e) : + string(indent, s, repeat('─', w-1), e) + end + end + if !isnothing(note) + markline *= " ── " + end + _printstyled(io, markline; fgcolor=color) + if !isnothing(note) + _printnote(io, indent, w, note, notecolor) + end +end + +""" +Print the lines of source code surrounding the given byte `range`, which is +highlighted with background `color` and markers in the text. +""" +function highlight(io::IO, source::SourceFile, range::UnitRange; + color=(120,70,70), context_inner_lines=1, context_lines_before=2, + context_lines_after=2, note=nothing, notecolor=nothing) + p = first(range) + q = last(range) + + x,y = source_line_range(source, p; + context_lines_before=context_lines_before, + context_lines_after=context_inner_lines) + a,b = source_line_range(source, p) + c,d = source_line_range(source, q) + z,w = source_line_range(source, q; + context_lines_before=context_inner_lines, + context_lines_after=context_lines_after) + + p_line = source_line(source, p) + q_line = source_line(source, q) + + marker_line_color = :light_black + + if p_line == q_line + # x----------------- + # a---p-------q----b + # # └───────┘ ── note + # -----------------w + + hitext = source[p:q] + print(io, source[x:p-1]) + _printstyled(io, hitext; bgcolor=color) + print(io, source[q+1:d]) + source[d] == '\n' || print(io, "\n") + _print_marker_line(io, source[a:p-1], hitext, true, true, marker_line_color, note, notecolor) + else + # x -------------- + # # ┌───── + # a---p----b + # --------------y + # --------------- + # z-------------- + # c --------q----d + # #───────────┘ ── note + # -----------------w + + prefix1 = source[a:p-1] + print(io, source[x:a-1]) + _print_marker_line(io, prefix1, source[p:b], false, false, marker_line_color, nothing, notecolor) + print(io, '\n') + print(io, prefix1) + if q_line - p_line - 1 <= 2*context_inner_lines + # The diagnostic range is compact and we show the whole thing + _printstyled(io, source[p:q]; bgcolor=color) + else + # Or large and we trucate the code to show only the region around the + # start and end of the error. + _printstyled(io, source[p:y]; bgcolor=color) + print(io, "⋮\n") + _printstyled(io, source[z:q]; bgcolor=color) + end + print(io, source[q+1:d]) + source[d] == '\n' || print(io, "\n") + qline = source[c:q] + _print_marker_line(io, "", qline, true, false, marker_line_color, note, notecolor) + end + if context_lines_after > 0 && d+1 < lastindex(source) + print(io, '\n') + w1 = source[w] == '\n' ? w - 1 : w + print(io, source[d+1:w1]) + end +end diff --git a/src/syntax_tree.jl b/src/syntax_tree.jl index 66cf8757..7563d69d 100644 --- a/src/syntax_tree.jl +++ b/src/syntax_tree.jl @@ -165,8 +165,11 @@ last_byte(node::AbstractSyntaxNode) = node.position + span(node) - 1 Get the full source text of a node. """ function sourcetext(node::AbstractSyntaxNode) - val_range = (node.position-1) .+ (1:span(node)) - view(node.source, val_range) + view(node.source, range(node)) +end + +function Base.range(node::AbstractSyntaxNode) + (node.position-1) .+ (1:span(node)) end source_line(node::AbstractSyntaxNode) = source_line(node.source, node.position) @@ -328,13 +331,12 @@ function child_position_span(node::SyntaxNode, path::Int...) n, n.position, span(n) end -""" -Print the code, highlighting the part covered by `node` at tree `path`. -""" -function highlight(io::IO, code::String, node, path::Int...; color=(40,40,70)) - node, p, span = child_position_span(node, path...) - q = p + span - print(io, code[1:p-1]) - _printstyled(io, code[p:q-1]; bgcolor=color) - print(io, code[q:end]) +function highlight(io::IO, node::SyntaxNode; kws...) + highlight(io, node.source, range(node); kws...) +end + +function highlight(io::IO, source::SourceFile, node::GreenNode, path::Int...; kws...) + _, p, span = child_position_span(node, path...) + q = p + span - 1 + highlight(io, source, p:q; kws...) end diff --git a/src/utils.jl b/src/utils.jl index 153d845a..f30f06f9 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -52,28 +52,76 @@ end #------------------------------------------------------------------------------- # Text printing/display utils +const _fg_color_codes = Dict( + :black => 30, + :red => 31, + :green => 32, + :yellow => 33, + :blue => 34, + :magenta => 35, + :cyan => 36, + :white => 37, + :light_black => 90, # gray + :light_red => 91, + :light_green => 92, + :light_yellow => 93, + :light_blue => 94, + :light_magenta => 95, + :light_cyan => 96, + :light_white => 97, +) + """ - Like printstyled, but allows providing RGB colors for true color terminals + _printstyled(io::IO, text; + fgcolor=nothing, bgcolor=nothing, href=nothing) + +Like Base.printstyled, but allows providing RGB colors for true color +terminals, both foreground and background colors, and hyperlinks. Colors may be +given as one of the standard color names as in `Base.printstyled`, an integer +for 256 color terms, or an (r,g,b) triple with `0 <= r <= 255` etc for true +color terminals. + +* `fgcolor` - set foreground color +* `bgcolor` - set background color +* `href` - set hyperlink reference """ -function _printstyled(io::IO, text; fgcolor=nothing, bgcolor=nothing) +function _printstyled(io::IO, text; fgcolor=nothing, bgcolor=nothing, href=nothing) + if (isnothing(fgcolor) && isnothing(bgcolor) && isnothing(href)) || !get(io, :color, false) + print(io, text) + return + end colcode = "" if !isnothing(fgcolor) - if length(fgcolor) != 3 || !all(0 .<= fgcolor .< 256) + if fgcolor isa Symbol && haskey(_fg_color_codes, fgcolor) + colcode *= "\e[$(_fg_color_codes[fgcolor])m" + elseif fgcolor isa Integer && 0 <= fgcolor <= 255 + colcode *= "\e[38;5;$(fgcolor)m" + elseif fgcolor isa Tuple && length(fgcolor) == 3 && all(0 .<= fgcolor .<= 255) + colcode *= "\e[38;2;$(fgcolor[1]);$(fgcolor[2]);$(fgcolor[3])m" + else error("Invalid ansi color $fgcolor") end - colcode *= "\e[38;2;$(fgcolor[1]);$(fgcolor[2]);$(fgcolor[3])m" end if !isnothing(bgcolor) - if length(bgcolor) != 3 || !all(0 .<= bgcolor .< 256) + if bgcolor isa Symbol && haskey(_fg_color_codes, bgcolor) + colcode *= "\e[$(10 + _fg_color_codes[bgcolor])m" + elseif bgcolor isa Integer && 0 <= bgcolor <= 255 + colcode *= "\e[48;5;$(bgcolor)m" + elseif bgcolor isa Tuple && length(bgcolor) == 3 && all(0 .<= bgcolor .<= 255) + colcode *= "\e[48;2;$(bgcolor[1]);$(bgcolor[2]);$(bgcolor[3])m" + else error("Invalid ansi color $bgcolor") end - colcode *= "\e[48;2;$(bgcolor[1]);$(bgcolor[2]);$(bgcolor[3])m" end colreset = "\e[0;0m" first = true for linepart in split(text, '\n') first || print(io, '\n') - print(io, colcode, linepart, colreset) + line = string(colcode, linepart, colreset) + if !isnothing(href) + line = "\e]8;;$href\e\\$line\e]8;;\e\\" + end + print(io, line) first = false end end diff --git a/test/parser_api.jl b/test/parser_api.jl index 3e25f322..1c4259a6 100644 --- a/test/parser_api.jl +++ b/test/parser_api.jl @@ -98,3 +98,29 @@ @test parseshow("1f1000", ignore_errors=true) == "(ErrorNumericOverflow)" end end + +@testset "ParseError printing" begin + try + JuliaSyntax.parse(JuliaSyntax.SyntaxNode, "a -- b -- c", filename="somefile.jl") + @assert false "error should be thrown" + catch exc + @test exc isa JuliaSyntax.ParseError + @test sprint(showerror, exc) == """ + ParseError: + # Error @ somefile.jl:1:3 + a -- b -- c + # └┘ ── invalid operator + # Error @ somefile.jl:1:8 + a -- b -- c + # └┘ ── invalid operator""" + file_url = JuliaSyntax._file_url("somefile.jl") + @test sprint(showerror, exc, context=:color=>true) == """ + ParseError: + \e[90m# Error @ \e[0;0m\e]8;;$file_url#1:3\e\\\e[90msomefile.jl:1:3\e[0;0m\e]8;;\e\\ + a \e[48;2;120;70;70m--\e[0;0m b -- c + \e[90m# └┘ ── \e[0;0m\e[91minvalid operator\e[0;0m + \e[90m# Error @ \e[0;0m\e]8;;$file_url#1:8\e\\\e[90msomefile.jl:1:8\e[0;0m\e]8;;\e\\ + a -- b \e[48;2;120;70;70m--\e[0;0m c + \e[90m# └┘ ── \e[0;0m\e[91minvalid operator\e[0;0m""" + end +end diff --git a/test/runtests.jl b/test/runtests.jl index 33847091..9bb664f1 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -42,6 +42,8 @@ include("test_utils.jl") Expr(:tuple, Expr(:parameters, :b), :a)) end +include("utils.jl") + @testset "Tokenize" begin include("tokenize.jl") end diff --git a/test/source_files.jl b/test/source_files.jl index 54bcccde..a9511c87 100644 --- a/test/source_files.jl +++ b/test/source_files.jl @@ -49,3 +49,99 @@ end end""" @test sourcetext(srcf) == "module Foo\nend" end + + +@testset "highlight()" begin + src = JuliaSyntax.SourceFile(""" + abcd + αβγδ + +-*/""") + + @test sprint(highlight, src, 1:4) == "abcd\n└──┘\nαβγδ\n+-*/" + @test sprint(highlight, src, 2:4) == "abcd\n#└─┘\nαβγδ\n+-*/" + @test sprint(highlight, src, 3:4) == "abcd\n# └┘\nαβγδ\n+-*/" + @test sprint(highlight, src, 4:4) == "abcd\n# ╙\nαβγδ\n+-*/" + @test sprint(highlight, src, 4:3) == "abcd\n# └\nαβγδ\n+-*/" + @test sprint(highlight, src, 5:5) == "abcd\n# └\nαβγδ\n+-*/" + + # multi-byte chars + @test sprint(highlight, src, 8:13) == """ + abcd + αβγδ + #└─┘ + +-*/""" + + # Multi-line ranges + @test sprint(highlight, src, 1:7) == """ + ┌─── + abcd + αβγδ + ┘ + +-*/""" + @test sprint(highlight, src, 2:7) == """ + #┌── + abcd + αβγδ + ┘ + +-*/""" + @test sprint(highlight, src, 2:9) == """ + #┌── + abcd + αβγδ + #┘ + +-*/""" + @test sprint(highlight, src, 4:9) == """ + # ┌ + abcd + αβγδ + #┘ + +-*/""" + @test sprint(highlight, src, 5:9) == """ + # ┌ + abcd + αβγδ + #┘ + +-*/""" + @test sprint(highlight, src, 1:18) == """ + ┌─── + abcd + αβγδ + +-*/ + #──┘""" + + # context lines + @test sprint(io->highlight(io, src, 8:13; + context_lines_before=0, + context_lines_after=0)) == """ + αβγδ + #└─┘""" + @test sprint(io->highlight(io, src, 8:13; context_lines_after=0)) == """ + abcd + αβγδ + #└─┘""" + @test sprint(io->highlight(io, src, 8:13; context_lines_before=0)) == """ + αβγδ + #└─┘ + +-*/""" + + # annotations + @test sprint(io->highlight(io, src, 8:13; note="hello")) == """ + abcd + αβγδ + #└─┘ ── hello + +-*/""" + @test sprint(io->highlight(io, src, 1:13; note="hello")) == """ + ┌─── + abcd + αβγδ + #──┘ ── hello + +-*/""" + + # colored output + @test sprint(io->highlight(io, src, 8:13; context_lines_after=0, note="hello", notecolor=:light_red), + context=:color=>true) == + "abcd\nα\e[48;2;120;70;70mβγδ\e[0;0m\n\e[90m#└─┘ ── \e[0;0m\e[91mhello\e[0;0m" + @test sprint(io->highlight(io, src, 1:13; context_lines_after=0, note="hello", notecolor=(255,0,0)), + context=:color=>true) == + "\e[90m┌───\e[0;0m\n\e[48;2;120;70;70mabcd\e[0;0m\n\e[48;2;120;70;70mαβγδ\e[0;0m\n\e[90m#──┘ ── \e[0;0m\e[38;2;255;0;0mhello\e[0;0m" +end diff --git a/test/syntax_tree.jl b/test/syntax_tree.jl index 30a77cdc..c6dd6585 100644 --- a/test/syntax_tree.jl +++ b/test/syntax_tree.jl @@ -26,8 +26,8 @@ # These tests are deliberately quite relaxed to avoid being too specific about display style @test occursin("line:col", str) @test occursin("call-i", str) - @test sprint(JuliaSyntax.highlight, tt, t, 1, 3) == "a*\e[48;2;40;40;70mb\e[0;0m + c" - @test sprint(JuliaSyntax.highlight, tt, t.raw, 5) == "a*b + \e[48;2;40;40;70mc\e[0;0m" + @test sprint(highlight, child(t, 1, 3)) == "a*b + c\n# ╙" + @test sprint(highlight, t.source, t.raw, 1, 3) == "a*b + c\n# ╙" # Pass-through field access node = child(t, 1, 1) diff --git a/test/test_utils.jl b/test/test_utils.jl index ada48040..2059f96a 100644 --- a/test/test_utils.jl +++ b/test/test_utils.jl @@ -26,7 +26,8 @@ using .JuliaSyntax: children, child, fl_parseall, - fl_parse + fl_parse, + highlight if VERSION < v"1.6" # Compat stuff which might not be in Base for older versions diff --git a/test/utils.jl b/test/utils.jl new file mode 100644 index 00000000..227077f6 --- /dev/null +++ b/test/utils.jl @@ -0,0 +1,17 @@ +@testset "_printstyled" begin + ps(str; kws...) = sprint(io->JuliaSyntax._printstyled(IOContext(io, :color=>true), str; kws...)) + + @test ps("XX"; fgcolor=:red) == "\e[31mXX\e[0;0m" + @test ps("XX"; fgcolor=42) == "\e[38;5;42mXX\e[0;0m" + @test ps("XX"; fgcolor=(10,100,200)) == "\e[38;2;10;100;200mXX\e[0;0m" + + ps("XX"; bgcolor=:red) == "\e[41mXX\e[0;0m" + @test ps("XX"; bgcolor=42) == "\e[48;5;42mXX\e[0;0m" + @test ps("XX"; bgcolor=(10,100,200)) == "\e[48;2;10;100;200mXX\e[0;0m" + + @test ps("XX"; href="https://www.example.com") == + "\e]8;;https://www.example.com\e\\XX\e[0;0m\e]8;;\e\\" + + @test ps("XX", fgcolor=:red, bgcolor=:green, href="https://www.example.com") == + "\e]8;;https://www.example.com\e\\\e[31m\e[42mXX\e[0;0m\e]8;;\e\\" +end From 774ece6dcbc5662b8d3fe10db0b1c1ed7ffeb2fd Mon Sep 17 00:00:00 2001 From: c42f Date: Sat, 11 Mar 2023 14:32:15 +1000 Subject: [PATCH 2/3] fixup! Rewrite source range `highlight()` --- src/diagnostics.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/diagnostics.jl b/src/diagnostics.jl index 31199bbf..40497251 100644 --- a/src/diagnostics.jl +++ b/src/diagnostics.jl @@ -46,7 +46,7 @@ Base.range(d::Diagnostic) = first_byte(d):last_byte(d) function _file_url(filename) @static if Sys.iswindows() # TODO: Test this with windows terminal - path = join(splitpath(abspath(filename)), '/') + path = replace(abspath(filename), '\\'=>'/') else path = abspath(filename) end From 549055e88ee9a64c276b0b20f6ec2e535b8ba904 Mon Sep 17 00:00:00 2001 From: c42f Date: Sat, 11 Mar 2023 15:34:29 +1000 Subject: [PATCH 3/3] fixup! Rewrite source range `highlight()` --- src/parse_stream.jl | 4 ++-- src/source_files.jl | 31 ++++++++++++------------------- test/diagnostics.jl | 12 ++++++++++++ test/source_files.jl | 32 ++++++++++++++++++++++++-------- 4 files changed, 50 insertions(+), 29 deletions(-) diff --git a/src/parse_stream.jl b/src/parse_stream.jl index 4157910c..f5a8bcd7 100644 --- a/src/parse_stream.jl +++ b/src/parse_stream.jl @@ -295,8 +295,8 @@ function Base.show(io::IO, mime::MIME"text/plain", stream::ParseStream) println(io, "ParseStream at position $(_next_byte(stream))") end -function show_diagnostics(io::IO, stream::ParseStream, code) - show_diagnostics(io, stream.diagnostics, code) +function show_diagnostics(io::IO, stream::ParseStream) + show_diagnostics(io, stream.diagnostics, sourcetext(stream)) end # We manage a pool of stream positions as parser working space diff --git a/src/source_files.jl b/src/source_files.jl index b3406315..d2903a5b 100644 --- a/src/source_files.jl +++ b/src/source_files.jl @@ -124,18 +124,6 @@ end #------------------------------------------------------------------------------- # Tools for highlighting source ranges - -function _printnote(io, indent, w, note::AbstractString, notecolor) - if isnothing(notecolor) - print(io, note) - else - _printstyled(io, note, fgcolor=notecolor) - end -end -function _printnote(io, indent, w, note::Function, notecolor) - note(io, indent, w) -end - function _print_marker_line(io, prefix_str, str, underline, singleline, color, note, notecolor) # Whitespace equivalent in length to `prefix_str` @@ -167,12 +155,16 @@ function _print_marker_line(io, prefix_str, str, underline, singleline, color, string(indent, s, repeat('─', w-1), e) end end - if !isnothing(note) + if note isa AbstractString markline *= " ── " end _printstyled(io, markline; fgcolor=color) if !isnothing(note) - _printnote(io, indent, w, note, notecolor) + if note isa AbstractString + _printstyled(io, note, fgcolor=notecolor) + else + note(io, indent, w) + end end end @@ -181,18 +173,19 @@ Print the lines of source code surrounding the given byte `range`, which is highlighted with background `color` and markers in the text. """ function highlight(io::IO, source::SourceFile, range::UnitRange; - color=(120,70,70), context_inner_lines=1, context_lines_before=2, - context_lines_after=2, note=nothing, notecolor=nothing) + color=(120,70,70), context_lines_before=2, + context_lines_inner=1, context_lines_after=2, + note=nothing, notecolor=nothing) p = first(range) q = last(range) x,y = source_line_range(source, p; context_lines_before=context_lines_before, - context_lines_after=context_inner_lines) + context_lines_after=context_lines_inner) a,b = source_line_range(source, p) c,d = source_line_range(source, q) z,w = source_line_range(source, q; - context_lines_before=context_inner_lines, + context_lines_before=context_lines_inner, context_lines_after=context_lines_after) p_line = source_line(source, p) @@ -228,7 +221,7 @@ function highlight(io::IO, source::SourceFile, range::UnitRange; _print_marker_line(io, prefix1, source[p:b], false, false, marker_line_color, nothing, notecolor) print(io, '\n') print(io, prefix1) - if q_line - p_line - 1 <= 2*context_inner_lines + if q_line - p_line - 1 <= 2*context_lines_inner # The diagnostic range is compact and we show the whole thing _printstyled(io, source[p:q]; bgcolor=color) else diff --git a/test/diagnostics.jl b/test/diagnostics.jl index 3324b2b0..1df79345 100644 --- a/test/diagnostics.jl +++ b/test/diagnostics.jl @@ -56,3 +56,15 @@ end Diagnostic(12, 13, :error, "invalid escape sequence") ] end + +@testset "diagnostic printing" begin + stream = JuliaSyntax.ParseStream("a -- b -- c") + JuliaSyntax.parse!(stream) + @test sprint(JuliaSyntax.show_diagnostics, stream) == """ + # Error @ line 1:3 + a -- b -- c + # └┘ ── invalid operator + # Error @ line 1:8 + a -- b -- c + # └┘ ── invalid operator""" +end diff --git a/test/source_files.jl b/test/source_files.jl index a9511c87..72455dfc 100644 --- a/test/source_files.jl +++ b/test/source_files.jl @@ -113,16 +113,22 @@ end @test sprint(io->highlight(io, src, 8:13; context_lines_before=0, context_lines_after=0)) == """ - αβγδ - #└─┘""" + αβγδ + #└─┘""" @test sprint(io->highlight(io, src, 8:13; context_lines_after=0)) == """ - abcd - αβγδ - #└─┘""" + abcd + αβγδ + #└─┘""" @test sprint(io->highlight(io, src, 8:13; context_lines_before=0)) == """ - αβγδ - #└─┘ - +-*/""" + αβγδ + #└─┘ + +-*/""" + @test sprint(io->highlight(io, src, 1:18; context_lines_inner=0)) == """ + ┌─── + abcd + ⋮ + +-*/ + #──┘""" # annotations @test sprint(io->highlight(io, src, 8:13; note="hello")) == """ @@ -136,6 +142,13 @@ end αβγδ #──┘ ── hello +-*/""" + @test sprint(io->highlight(io, src, 8:13; + note=(io,indent,w)->print(io, "\n$indent$('!'^w) hello"))) == """ + abcd + αβγδ + #└─┘ + #!!! hello + +-*/""" # colored output @test sprint(io->highlight(io, src, 8:13; context_lines_after=0, note="hello", notecolor=:light_red), @@ -144,4 +157,7 @@ end @test sprint(io->highlight(io, src, 1:13; context_lines_after=0, note="hello", notecolor=(255,0,0)), context=:color=>true) == "\e[90m┌───\e[0;0m\n\e[48;2;120;70;70mabcd\e[0;0m\n\e[48;2;120;70;70mαβγδ\e[0;0m\n\e[90m#──┘ ── \e[0;0m\e[38;2;255;0;0mhello\e[0;0m" + @test sprint(io->highlight(io, src, 1:18, context_lines_inner=0), + context=:color=>true) == + "\e[90m┌───\e[0;0m\n\e[48;2;120;70;70mabcd\e[0;0m\n\e[48;2;120;70;70m\e[0;0m⋮\n\e[48;2;120;70;70m+-*/\e[0;0m\n\e[90m#──┘\e[0;0m" end