Skip to content

Commit

Permalink
Integrate Cthulhu with VSCode - show types in source code (#469)
Browse files Browse the repository at this point in the history
  • Loading branch information
Zentrik authored Aug 20, 2023
1 parent 7f24535 commit e7c424e
Show file tree
Hide file tree
Showing 17 changed files with 1,332 additions and 26 deletions.
1 change: 1 addition & 0 deletions TypedSyntax/src/TypedSyntax.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ using CodeTracking
export TypedSyntaxNode

include("node.jl")
include("vscode.jl")
include("show.jl")

end
3 changes: 2 additions & 1 deletion TypedSyntax/src/node.jl
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ function tsn_and_mappings(m::Method, src::CodeInfo, @nospecialize(rt); warn::Boo
end

function tsn_and_mappings(m::Method, src::CodeInfo, @nospecialize(rt), sourcetext::AbstractString, lineno::Integer; warn::Bool=true, strip_macros::Bool=false, kwargs...)
rootnode = JuliaSyntax.parsestmt(SyntaxNode, sourcetext; filename=string(m.file), first_line=lineno, kwargs...)
filename = isnothing(functionloc(m)[1]) ? string(m.file) : functionloc(m)[1]
rootnode = JuliaSyntax.parsestmt(SyntaxNode, sourcetext; filename=filename, first_line=lineno, kwargs...)
if strip_macros
rootnode = get_function_def(rootnode)
if !is_function_def(rootnode)
Expand Down
36 changes: 25 additions & 11 deletions TypedSyntax/src/show.jl
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ function Base.printstyled(io::IO, rootnode::MaybeTypedSyntaxNode;
sig, body = children(rootnode)
type_annotate, pre, pre2, post = type_annotation_mode(sig, rt; type_annotations, hide_type_stable)
position = show_src_expr(io, sig, position, pre, pre2; type_annotations, iswarn, hide_type_stable, nd)
type_annotate && show_annotation(io, rt, post; iswarn)
type_annotate && show_annotation(io, rt, post, rootnode.source, position; iswarn)
rootnode = body
end
position = show_src_expr(io, rootnode, position, "", ""; type_annotations, iswarn, hide_type_stable, nd)
Expand All @@ -55,6 +55,14 @@ Base.printstyled(rootnode::MaybeTypedSyntaxNode; kwargs...) = printstyled(stdout

ndigits_linenumbers(node::AbstractSyntaxNode, idxend = last_byte(node)) = ndigits(node.source.first_line + nlines(node.source, idxend) - 1)

function _print(io::IO, x, node, position)
print(io, x)

if !isempty(x)
add_hint!(get(io, :inlay_hints, nothing), x, node, position+1)
end
end

function show_src_expr(io::IO, node::MaybeTypedSyntaxNode, position::Int, pre::String, pre2::String; type_annotations::Bool=true, iswarn::Bool=false, hide_type_stable::Bool=false, nd::Int)
_lastidx = last_byte(node)
position = catchup(io, node, position, nd)
Expand All @@ -64,13 +72,13 @@ function show_src_expr(io::IO, node::MaybeTypedSyntaxNode, position::Int, pre::S
position = catchup(io, first(children(node)), position, nd)
end
end
print(io, pre)
_print(io, pre, node.source, position)
for (i, child) in enumerate(children(node))
i == 2 && print(io, pre2)
i == 2 && _print(io, pre2, node.source, position)
cT = gettyp(child)
ctype_annotate, cpre, cpre2, cpost = type_annotation_mode(child, cT; type_annotations, hide_type_stable)
position = show_src_expr(io, child, position, cpre, cpre2; type_annotations, iswarn, hide_type_stable, nd)
ctype_annotate && show_annotation(io, cT, cpost; iswarn)
ctype_annotate && show_annotation(io, cT, cpost, node.source, position; iswarn)
end
return catchup(io, node, position, nd, _lastidx+1)
end
Expand Down Expand Up @@ -100,19 +108,25 @@ function type_annotation_mode(node, @nospecialize(T); type_annotations::Bool, hi
return type_annotate, pre, pre2, post
end

function show_annotation(io, @nospecialize(T), post=""; iswarn::Bool)
function show_annotation(io, @nospecialize(T), post, node, position; iswarn::Bool)
diagnostics = get(io, :diagnostics, nothing)
inlay_hints = get(io, :inlay_hints, nothing)

print(io, post)
if iswarn
color = !is_type_unstable(T) ? :cyan :
is_small_union_or_tunion(T) ? :yellow : :red
printstyled(io, "::", T; color)
T_str = string(T)
if iswarn && is_type_unstable(T)
color = is_small_union_or_tunion(T) ? :yellow : :red
printstyled(io, "::", T_str; color)
add_diagnostic!(diagnostics, node, position+1, is_small_union_or_tunion(T) ? DiagnosticKinds.Information : DiagnosticKinds.Warning)
add_hint!(inlay_hints, string(post, "::", T_str), node, position+1; kind=InlayHintKinds.Nothing)
else
printstyled(io, "::", T; color=:cyan)
printstyled(io, "::", T_str; color=:cyan)
add_hint!(inlay_hints, string(post, "::", T_str), node, position+1; kind=InlayHintKinds.Type)
end
end

print_linenumber(io::IO, node::MaybeTypedSyntaxNode, position::Int, nd::Int) =
print_linenumber(io, source_line(node.source, position), nd)
print_linenumber(io, source_line(node.source, position+1), nd)
print_linenumber(io::IO, ln::Int, nd::Int) = printstyled(io, lpad(ln, nd), " "; color=:light_black)

# Do any "overdue" printing, generating a line number if needed. Mostly, this catches whitespace.
Expand Down
91 changes: 91 additions & 0 deletions TypedSyntax/src/vscode.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
diagnostics_available_vscode() = isdefined(Main, :VSCodeServer) && Main.VSCodeServer isa Module && isdefined(Main.VSCodeServer, :DIAGNOSTICS_ENABLED) && Main.VSCodeServer.DIAGNOSTICS_ENABLED[]
inlay_hints_available_vscode() = isdefined(Main, :VSCodeServer) && Main.VSCodeServer isa Module && isdefined(Main.VSCodeServer, :INLAY_HINTS_ENABLED) && Main.VSCodeServer.INLAY_HINTS_ENABLED[]

module DiagnosticKinds
@enum T Error=0 Warning=1 Information=2 Hint=3
end

struct Diagnostic
path::String
line::Int # 1-based indexing
severity::DiagnosticKinds.T
msg::String
end

to_vscode_type(x::Diagnostic) = (msg=x.msg, path = x.path, line = x.line, severity = Int(x.severity))
function Base.show(io::IO, ::MIME"application/vnd.julia-vscode.diagnostics", diagnostics::AbstractVector{Diagnostic})
return (
source = "Cthulhu",
items = to_vscode_type.(diagnostics),
)
end

add_diagnostic!(::Nothing, node, position, severity) = nothing
function add_diagnostic!(diagnostics, node, position, severity)
file_path = node.filename
line = source_line(node, position)
push!(diagnostics, Diagnostic(file_path, line, severity, "Unstable Type"))
end

function clear_diagnostics_vscode()
if diagnostics_available_vscode()
display(Main.VSCodeServer.InlineDisplay(false), TypedSyntax.Diagnostic[])
end
end

function display_diagnostics_vscode(diagnostics)
if diagnostics_available_vscode() && !isnothing(diagnostics)
# InlineDisplay(false) means we don't print to REPL
display(Main.VSCodeServer.InlineDisplay(false), diagnostics)
end
end
display_diagnostics_vscode(io::IO) = display_diagnostics_vscode(get(io, :diagnostics, nothing))

const InlayHintKinds = (Type=1, Parameter=2, Nothing=nothing)

struct InlayHint
line::Int # 1-based indexing
column::Int # 1-based indexing
label::String
kind::Union{Nothing, Int}
end

to_vscode_type(x::InlayHint) = (position=(x.line, x.column), label=x.label, kind=x.kind)
function Base.show(io::IO, ::MIME"application/vnd.julia-vscode.inlayHints", inlay_hints_by_file::Dict{T, Vector{InlayHint}}) where T
if inlay_hints_available_vscode()
return Dict{T, Vector{NamedTuple{(:position, :label, :kind), Tuple{Tuple{Int, Int}, String, Union{Nothing, Int}}}}}(
filepath => to_vscode_type.(inlay_hints) for (filepath, inlay_hints) in inlay_hints_by_file
)
end
return nothing
end

add_hint!(::Nothing, message, node, position; kind=InlayHintKinds.Type) = nothing
function add_hint!(inlay_hints, message, node, position; kind=InlayHintKinds.Type)
filepath = node.filename
line, column = source_location(node, position)

if filepath keys(inlay_hints)
inlay_hints[filepath] = InlayHint[]
end
push!(inlay_hints[filepath], InlayHint(line-1, column-1, message, kind))
end

function clear_inlay_hints_vscode()
if inlay_hints_available_vscode()
display(Main.VSCodeServer.InlineDisplay(false), Dict{String, Vector{TypedSyntax.InlayHint}}())
end
end

function display_inlay_hints_vscode(inlay_hints)
if inlay_hints_available_vscode() && !isnothing(inlay_hints)
# InlineDisplay(false) means we don't print to REPL
display(Main.VSCodeServer.InlineDisplay(false), inlay_hints)
end
end
display_inlay_hints_vscode(io::IO) = display_inlay_hints_vscode(get(io, :inlay_hints, nothing))

function clear_all_vscode()
clear_diagnostics_vscode()
clear_inlay_hints_vscode()
end
21 changes: 21 additions & 0 deletions TypedSyntax/test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -634,3 +634,24 @@ end
if parse(Bool, get(ENV, "CI", "false"))
include("exhaustive.jl")
end

module VSCodeServer
struct InlineDisplay
is_repl::Bool
end
const INLAY_HINTS_ENABLED = Ref(true)
const DIAGNOSTICS_ENABLED = Ref(true)

function Base.display(d::InlineDisplay, x)
return nothing
end
end
module TestVSCodeExt # stops modules defined in test files from overwriting stuff from previous test
using Test, ..VSCodeServer

@testset "VSCode TypedSyntax.jl" begin
@testset "test_vscode.jl" begin
include("test_vscode.jl")
end
end
end
6 changes: 6 additions & 0 deletions TypedSyntax/test/test_module.jl
Original file line number Diff line number Diff line change
Expand Up @@ -223,4 +223,10 @@ const T426 = Dict{Type{<:Dates.Period}, Bool}(
# Issue #458
f458() = return

function fVSCode(x)
z = x + 1
y = 2 * z
return y + (x > 0 ? -1 : 1.0)
end

end
Loading

0 comments on commit e7c424e

Please sign in to comment.