Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite source range highlight() #215

Merged
merged 3 commits into from
Mar 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 24 additions & 54 deletions src/diagnostics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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 = replace(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") :
Expand All @@ -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},
Expand Down
7 changes: 4 additions & 3 deletions src/parse_stream.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions src/parser.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
126 changes: 124 additions & 2 deletions src/source_files.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -120,3 +120,125 @@ 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 _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 note isa AbstractString
markline *= " ── "
end
_printstyled(io, markline; fgcolor=color)
if !isnothing(note)
if note isa AbstractString
_printstyled(io, note, fgcolor=notecolor)
else
note(io, indent, w)
end
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_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_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_lines_inner,
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_lines_inner
# 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
24 changes: 13 additions & 11 deletions src/syntax_tree.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Loading