mirror of https://github.com/fredrikekre/Runic.jl
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
365 lines
14 KiB
365 lines
14 KiB
# SPDX-License-Identifier: MIT |
|
|
|
# This is the runestone where all the formatting transformations are implemented. |
|
|
|
function trim_trailing_whitespace(ctx::Context, node::JuliaSyntax.GreenNode) |
|
JuliaSyntax.kind(node) === K"NewlineWs" || return nothing |
|
@assert !JuliaSyntax.haschildren(node) |
|
str = String(node_bytes(ctx, node)) |
|
str′ = replace(str, r"\h*(\r\n|\r|\n)" => '\n') |
|
# If the next sibling is also a NewlineWs we can trim trailing |
|
# whitespace from this node too |
|
next_kind = next_sibling_kind(ctx) |
|
if next_kind === K"NewlineWs" |
|
# str′ = replace(str′, r"(\r\n|\r|\n)\h*" => '\n') |
|
str′ = replace(str′, r"\n\h*" => '\n') |
|
end |
|
if str == str′ |
|
return nothing |
|
end |
|
# Write new bytes and reset the stream |
|
nb = write_and_reset(ctx, str′) |
|
@assert nb != JuliaSyntax.span(node) |
|
# Create new node and return it |
|
node′ = JuliaSyntax.GreenNode(JuliaSyntax.head(node), nb, ()) |
|
return node′ |
|
end |
|
|
|
function format_hex_literals(ctx::Context, node::JuliaSyntax.GreenNode) |
|
JuliaSyntax.kind(node) === K"HexInt" || return nothing |
|
@assert JuliaSyntax.flags(node) == 0 |
|
@assert !JuliaSyntax.haschildren(node) |
|
span = JuliaSyntax.span(node) |
|
@assert span > 2 # 0x prefix + something more |
|
# Target spans(0x + maximum chars for formatted UInt8, UInt16, UInt32, UInt64, UInt128) |
|
target_spans = 2 .+ (2, 4, 8, 16, 32) |
|
if span >= 34 || span in target_spans |
|
# Do nothing: correctly formatted or a BigInt hex literal |
|
return nothing |
|
end |
|
# Insert leading zeros |
|
i = findfirst(x -> x > span, target_spans)::Int |
|
bytes = node_bytes(ctx, node) |
|
while length(bytes) < target_spans[i] |
|
insert!(bytes, 3, '0') |
|
end |
|
nb = write_and_reset(ctx, bytes) |
|
@assert nb == length(bytes) == target_spans[i] |
|
# Create new node and return it |
|
node′ = JuliaSyntax.GreenNode(JuliaSyntax.head(node), nb, ()) |
|
return node′ |
|
end |
|
|
|
function format_oct_literals(ctx::Context, node::JuliaSyntax.GreenNode) |
|
JuliaSyntax.kind(node) === K"OctInt" || return nothing |
|
@assert JuliaSyntax.flags(node) == 0 |
|
@assert !JuliaSyntax.haschildren(node) |
|
span = JuliaSyntax.span(node) |
|
@assert span > 2 # 0o prefix + something more |
|
# Padding depends on the value of the literal... |
|
str = String(node_bytes(ctx, node)) |
|
n = tryparse(UInt128, str) |
|
if n === nothing |
|
# Do nothing: BigInt oct literal |
|
return nothing |
|
end |
|
# Compute the target span |
|
target_span_from_value = |
|
n <= typemax(UInt8) ? 5 : n <= typemax(UInt16) ? 8 : |
|
n <= typemax(UInt32) ? 13 : n <= typemax(UInt64) ? 24 : |
|
n <= typemax(UInt128) ? 45 : error("unreachable") |
|
target_spans = (5, 8, 13, 24, 45) |
|
i = findfirst(x -> x >= span, target_spans)::Int |
|
target_span_from_source = target_spans[i] |
|
target_span = max(target_span_from_value, target_span_from_source) |
|
if span == target_span |
|
# Do nothing: correctly formatted oct literal |
|
return nothing |
|
end |
|
# Insert leading zeros |
|
bytes = node_bytes(ctx, node) |
|
while length(bytes) < target_span |
|
insert!(bytes, 3, '0') |
|
end |
|
nb = write_and_reset(ctx, bytes) |
|
@assert nb == length(bytes) == target_span |
|
# Create new node and return it |
|
node′ = JuliaSyntax.GreenNode(JuliaSyntax.head(node), nb, ()) |
|
return node′ |
|
end |
|
|
|
function format_float_literals(ctx::Context, node::JuliaSyntax.GreenNode) |
|
JuliaSyntax.kind(node) in KSet"Float Float32" || return nothing |
|
@assert JuliaSyntax.flags(node) == 0 |
|
@assert !JuliaSyntax.haschildren(node) |
|
str = String(node_bytes(ctx, node)) |
|
# Check and shortcut the happy path first |
|
r = r""" |
|
^ |
|
(?:[+-])? # Optional sign |
|
(?:(?:[1-9]\d*)|0) # Non-zero followed by any digit, or just a single zero |
|
\. # Decimal point |
|
(?:(?:\d*[1-9])|0) # Any digit with a final nonzero, or just a single zero |
|
(?:[ef][+-]?[1-9]\d*)? |
|
$ |
|
"""x |
|
if occursin(r, str) |
|
return nothing |
|
end |
|
if occursin('_', str) || startswith(str, "0x") |
|
# TODO: Hex floats and floats with underscores are ignored |
|
return nothing |
|
end |
|
# Split up the pieces |
|
r = r"^(?<sgn>[+-])?(?<int>\d*)(?:\.?(?<frac>\d*))?(?:(?<epm>[eEf][+-]?)(?<exp>\d+))?$" |
|
m = match(r, str) |
|
io = IOBuffer() # TODO: Could be reused? |
|
# Write the sign part |
|
if (sgn = m[:sgn]; sgn !== nothing) |
|
write(io, sgn) |
|
end |
|
# Strip leading zeros from integral part |
|
int_part = isempty(m[:int]) ? "0" : m[:int] |
|
int_part = replace(int_part, r"^0*((?:[1-9]\d*)|0)$" => s"\1") |
|
write(io, int_part) |
|
# Always write the decimal point |
|
write(io, ".") |
|
# Strip trailing zeros from fractional part |
|
frac_part = isempty(m[:frac]) ? "0" : m[:frac] |
|
frac_part = replace(frac_part, r"^((?:\d*[1-9])|0)0*$" => s"\1") |
|
write(io, frac_part) |
|
# Write the exponent part |
|
if m[:epm] !== nothing |
|
write(io, replace(m[:epm], "E" => "e")) |
|
@assert m[:exp] !== nothing |
|
# Strip leading zeros from integral part |
|
exp_part = isempty(m[:exp]) ? "0" : m[:exp] |
|
exp_part = replace(exp_part, r"^0*((?:[1-9]\d*)|0)$" => s"\1") |
|
write(io, exp_part) |
|
end |
|
bytes = take!(io) |
|
nb = write_and_reset(ctx, bytes) |
|
@assert nb == length(bytes) |
|
# Create new node and return it |
|
node′ = JuliaSyntax.GreenNode(JuliaSyntax.head(node), nb, ()) |
|
return node′ |
|
end |
|
|
|
# Insert space around `x`, where `x` can be operators, assignments, etc. with the pattern: |
|
# `<something><space><x><space><something>`, for example the spaces around `+` and `=` in |
|
# `a = x + y`. |
|
function spaces_around_x(ctx::Context, node::JuliaSyntax.GreenNode, is_x::F) where F |
|
# TODO: So much boilerplate here... |
|
@assert JuliaSyntax.haschildren(node) |
|
# TODO: Can't handle NewlineWs here right now |
|
if any(JuliaSyntax.kind(c) === K"NewlineWs" for c in JuliaSyntax.children(node)) |
|
return nothing |
|
end |
|
|
|
children = verified_children(node) |
|
children′ = children |
|
any_changes = false |
|
original_bytes = node_bytes(ctx, node) |
|
span_sum = 0 |
|
pos = position(ctx.fmt_io) |
|
ws = JuliaSyntax.GreenNode( |
|
JuliaSyntax.SyntaxHead(K"Whitespace", JuliaSyntax.TRIVIA_FLAG), 1, (), |
|
) |
|
|
|
# Toggle for whether we are currently looking for whitespace or not |
|
looking_for_whitespace = false |
|
looking_for_x = false |
|
|
|
for (i, child) in pairs(children) |
|
span_sum += JuliaSyntax.span(child) |
|
if i == 1 && JuliaSyntax.kind(child) === K"Whitespace" |
|
# If the first child is whitespace it will be accepted as is even if the span is |
|
# larger than one since we don't look behind. The whitespace pass for the parent |
|
# node should trim it later (if not already done). |
|
accept_node!(ctx, child) |
|
@assert !any_changes |
|
looking_for_whitespace = false |
|
elseif looking_for_whitespace |
|
if JuliaSyntax.kind(child) === K"Whitespace" && JuliaSyntax.span(child) == 1 |
|
# All good, just advance the IO |
|
accept_node!(ctx, child) |
|
any_changes && push!(children′, child) |
|
looking_for_whitespace = false |
|
elseif JuliaSyntax.kind(child) === K"Whitespace" |
|
# Whitespace node but replace since not single space |
|
any_changes = true |
|
if children′ === children |
|
children′ = children[1:i - 1] |
|
end |
|
push!(children′, ws) |
|
write_and_reset(ctx, " ") |
|
accept_node!(ctx, ws) |
|
# Re-write bytes for remaining children |
|
remaining_bytes = @view original_bytes[(span_sum + 1):end] |
|
write_and_reset(ctx, remaining_bytes) |
|
looking_for_whitespace = false |
|
elseif JuliaSyntax.haschildren(child) && |
|
JuliaSyntax.kind(first_leaf(child)) === K"Whitespace" |
|
# Whitespace found at the beginning of next child. |
|
child_ws = first_leaf(child) |
|
looking_for_whitespace = JuliaSyntax.kind(last_leaf(child)) !== K"Whitespace" |
|
if JuliaSyntax.span(child_ws) == 1 |
|
# Accept the node |
|
accept_node!(ctx, child) |
|
any_changes && push!(children′, child) |
|
else |
|
# Replace the whitespace node of the child |
|
child′ = replace_first_leaf(child, ws) |
|
@assert JuliaSyntax.span(child′) == JuliaSyntax.span(child) - JuliaSyntax.span(child_ws) + 1 |
|
bytes_to_skip = JuliaSyntax.span(child) - JuliaSyntax.span(child′) |
|
@assert bytes_to_skip > 0 |
|
remaining_bytes_inclusive = |
|
@view original_bytes[(span_sum + 1 + bytes_to_skip - JuliaSyntax.span(child)):end] |
|
write_and_reset(ctx, remaining_bytes_inclusive) |
|
any_changes = true |
|
if children′ === children |
|
children′ = children[1:i - 1] |
|
end |
|
push!(children′, child′) |
|
end |
|
else |
|
# Not a whitespace node, insert one |
|
any_changes = true |
|
if children′ === children |
|
children′ = children[1:i - 1] |
|
end |
|
push!(children′, ws) |
|
write_and_reset(ctx, " ") |
|
accept_node!(ctx, ws) |
|
# Write and accept the node |
|
push!(children′, child) |
|
remaining_bytes_inclusive = |
|
@view original_bytes[(span_sum + 1 - JuliaSyntax.span(child)):end] |
|
write_and_reset(ctx, remaining_bytes_inclusive) |
|
accept_node!(ctx, child) |
|
looking_for_whitespace = JuliaSyntax.kind(last_leaf(child)) !== K"Whitespace" |
|
if looking_for_x |
|
@assert is_x(child)::Bool |
|
end |
|
looking_for_x = !looking_for_x |
|
end |
|
else # !expect_ws |
|
if looking_for_x |
|
@assert is_x(child)::Bool |
|
end |
|
@assert JuliaSyntax.kind(child) !== K"Whitespace" # This would be weird, I think? |
|
any_changes && push!(children′, child) |
|
accept_node!(ctx, child) |
|
looking_for_whitespace = JuliaSyntax.kind(last_leaf(child)) !== K"Whitespace" |
|
looking_for_x = !looking_for_x |
|
end |
|
end |
|
# Reset stream |
|
seek(ctx.fmt_io, pos) |
|
if any_changes |
|
# Create new node and return it |
|
span′ = mapreduce(JuliaSyntax.span, +, children′; init = 0) |
|
node′ = JuliaSyntax.GreenNode(JuliaSyntax.head(node), span′, children′) |
|
return node′ |
|
else |
|
return nothing |
|
end |
|
end |
|
|
|
# This pass handles spaces around infix operator calls, comparison chains, and |
|
# <: and >: operators. |
|
function spaces_around_operators(ctx::Context, node::JuliaSyntax.GreenNode) |
|
if !( |
|
(is_infix_op_call(node) && !(JuliaSyntax.kind(infix_op_call_op(node)) in KSet": ^")) || |
|
(JuliaSyntax.kind(node) in KSet"<: >:" && n_children(node) == 3) || |
|
(JuliaSyntax.kind(node) === K"comparison" && !JuliaSyntax.is_trivia(node)) |
|
) |
|
return nothing |
|
end |
|
@assert JuliaSyntax.kind(node) in KSet"call comparison <: >:" |
|
is_x = x -> is_operator_leaf(x) || is_comparison_leaf(x) |
|
return spaces_around_x(ctx, node, is_x) |
|
end |
|
|
|
function spaces_around_assignments(ctx::Context, node::JuliaSyntax.GreenNode) |
|
if !(is_assignment(node) && !is_leaf(node) ) |
|
return nothing |
|
end |
|
# for-loop nodes are of kind K"=" even when `in` or `∈` is used so we need to |
|
# include these kinds in the predicate too. |
|
is_x = x -> is_assignment(x) || JuliaSyntax.kind(x) in KSet"in ∈" |
|
return spaces_around_x(ctx, node, is_x) |
|
end |
|
|
|
# Opposite of `spaces_around_x`: remove spaces around `x` |
|
function no_spaces_around_x(ctx::Context, node::JuliaSyntax.GreenNode, is_x::F) where F |
|
@assert JuliaSyntax.haschildren(node) |
|
# TODO: Can't handle NewlineWs here right now |
|
if any(JuliaSyntax.kind(c) === K"NewlineWs" for c in JuliaSyntax.children(node)) |
|
return nothing |
|
end |
|
|
|
children = verified_children(node) |
|
children′ = children |
|
any_changes = false |
|
original_bytes = node_bytes(ctx, node) |
|
span_sum = 0 |
|
pos = position(ctx.fmt_io) |
|
|
|
looking_for_x = false |
|
|
|
# K"::", K"<:", and K">:" are special cases here since they can be used without an LHS |
|
# in e.g. `f(::Int) = ...` and `Vector{<:Real}`. |
|
if JuliaSyntax.kind(node) in KSet":: <: >:" |
|
looking_for_x = is_x(first_non_whitespace_child(node))::Bool |
|
end |
|
|
|
for (i, child) in pairs(children) |
|
span_sum += JuliaSyntax.span(child) |
|
if (i == 1 || i == length(children)) && JuliaSyntax.kind(child) === K"Whitespace" |
|
accept_node!(ctx, child) |
|
any_changes && push!(children′, child) |
|
elseif JuliaSyntax.kind(child) === K"Whitespace" |
|
# Ignore it but need to copy children and re-write bytes |
|
any_changes = true |
|
if children′ === children |
|
children′ = children[1:i - 1] |
|
end |
|
remaining_bytes = @view original_bytes[(span_sum + 1):end] |
|
write_and_reset(ctx, remaining_bytes) |
|
else |
|
@assert JuliaSyntax.kind(child) !== K"Whitespace" |
|
if looking_for_x |
|
@assert is_x(child)::Bool |
|
end |
|
any_changes && push!(children′, child) |
|
accept_node!(ctx, child) |
|
looking_for_x = !looking_for_x |
|
end |
|
end |
|
# Reset stream |
|
seek(ctx.fmt_io, pos) |
|
if any_changes |
|
# Create new node and return it |
|
span′ = mapreduce(JuliaSyntax.span, +, children′; init = 0) |
|
@assert span′ < JuliaSyntax.span(node) |
|
node′ = JuliaSyntax.GreenNode(JuliaSyntax.head(node), span′, children′) |
|
return node′ |
|
else |
|
return nothing |
|
end |
|
end |
|
|
|
# no spaces around `:`, `^`, and `::` |
|
function no_spaces_around_colon_etc(ctx::Context, node::JuliaSyntax.GreenNode) |
|
if !( |
|
(is_infix_op_call(node) && JuliaSyntax.kind(infix_op_call_op(node)) in KSet": ^") || |
|
(JuliaSyntax.kind(node) === K"::" && !is_leaf(node)) || |
|
(JuliaSyntax.kind(node) in KSet"<: >:" && n_children(node) == 2) |
|
) |
|
return nothing |
|
end |
|
@assert JuliaSyntax.kind(node) in KSet"call :: <: >:" |
|
is_x = x -> is_leaf(x) && JuliaSyntax.kind(x) in KSet": ^ :: <: >:" |
|
return no_spaces_around_x(ctx, node, is_x) |
|
end
|
|
|