Skip to content
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
43 changes: 38 additions & 5 deletions src/layer_signatures.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@

Describes a single parameter of a function signature in signature help.

- `label::String`: The parameter's textual label as it appears in the signature.
- `label::Union{String,Tuple{Int,Int}}`: The parameter as it appears in the
signature — either the exact substring, or a `[start, end)` UTF-16 offset range
into the signature label (LSP `ParameterInformation.label`).
- `documentation::Union{String,Nothing}`: Optional documentation for the parameter.
"""
struct ParameterInfo
label::String
label::Union{String,Tuple{Int,Int}}
documentation::Union{String,Nothing}
end

Expand Down Expand Up @@ -132,14 +134,45 @@ end
_sig_type_str(@nospecialize(t), pred) =
sprint((io, x) -> show(IOContext(io, :ss_shorten => pred), x), t)

# Number of UTF-16 code units in `s`. `ParameterInformation` label offsets are
# counted in UTF-16 code units (LSP spec), matching how the client indexes the
# signature label.
_utf16_length(s::AbstractString) = sum(c -> codepoint(c) >= 0x10000 ? 2 : 1, s; init=0)

# Text of a single SymbolServer method parameter exactly as it is rendered in
# the signature label by `Base.print(io, ::MethodStore)` under the same
# `:ss_shorten`/`:ss_omit_any` context: `name::type`, `::type` for an unnamed
# (`#unused#`) argument, or just `name` when the `::Any` annotation is omitted.
function _ss_param_text(a, pred)
buf = IOBuffer()
io = IOContext(buf, :ss_shorten => pred)
a[1] === Symbol("#unused#") || print(io, a[1])
SymbolServer.isfakeany(a[2]) || print(io, "::", a[2])
return String(take!(buf))
end

function _get_signatures(b::T, tls::StaticLint.Scope, sigs::Vector{SignatureInfo}, env, meta_dict) where T <: Union{SymbolServer.FunctionStore,SymbolServer.DataTypeStore}
pred = _sig_shorten_pred(env)
StaticLint.iterate_over_ss_methods(b, tls, env, function (m)
label = sprint((io, x) -> print(IOContext(io, :ss_shorten => pred, :ss_omit_any => true), x), m)
params = [ParameterInfo(
string(a[1]),
# `ParameterInformation.label` is a `[start, end)` UTF-16 offset range
# into the signature label (LSP spec). The label starts with `name(` and
# joins parameters with `, `, so each parameter's span follows from the
# accumulated rendered widths — a positional map, so it can never emit
# the `#unused#` placeholder and stays unambiguous even when two
# parameters render identically (e.g. `::Any, ::Any`).
off = _utf16_length(string(m.name)) + 1 # advance past "name("
n = length(m.sig)
params = ParameterInfo[]
for (i, a) in enumerate(m.sig)
w = _utf16_length(_ss_param_text(a, pred))
push!(params, ParameterInfo(
(off, off + w),
SymbolServer.isfakeany(a[2]) ? "" : _sig_type_str(a[2], pred)
) for a in m.sig]
))
off += w
i == n || (off += 2) # ", " separator
end
push!(sigs, SignatureInfo(label, "", params))
return false
end)
Expand Down
44 changes: 43 additions & 1 deletion test/test_signatures.jl
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ end
@test occursin("identity(x) in Base", s.label)
@test !occursin("Core.Any", s.label)
@test !occursin("::", split(s.label, " in ")[1])
@test s.parameters[1].label == "x"
@test s.parameters[1].label == (9, 10) # the `x` in `identity(x)`
@test s.parameters[1].documentation == ""

# `print(io::IO, ...)` — the exported `IO` type must render without its
Expand All @@ -224,6 +224,48 @@ end
@test any(s -> any(p -> p.documentation == "IO", s.parameters), psigs)
end

@testitem "Signatures: parameter labels are offset ranges into the signature" begin
using JuliaWorkspaces: JuliaWorkspace, add_file!, TextFile, SourceText, get_signature_help
using JuliaWorkspaces.URIs2: URI

# `get` has stdlib methods with unnamed arguments (e.g. `get(::Base.EnvDict,
# k, def)`). The LSP spec requires each parameter label to be either a
# substring of the signature label or a `[start, end)` UTF-16 offset range
# into it — never the internal placeholder `#unused#`. We emit offset ranges,
# so each range must select exactly the parameter's text.
source = """
get(
"""

jw = JuliaWorkspace()
uri = URI("file:///sigunused/test.jl")
add_file!(jw, TextFile(uri, SourceText(source, "julia")))

# UTF-16 code-unit slice of `s` for a 0-based `[start, end)` range. The test
# signatures are ASCII, so this coincides with a plain character slice.
function utf16_slice(s, range)
units = Char[]
for c in s
push!(units, c)
codepoint(c) >= 0x10000 && push!(units, c) # surrogate pair filler
end
return String(units[(range[1] + 1):range[2]])
end

result = get_signature_help(jw, uri, ncodeunits("get("))
@test !isempty(result.signatures)
params = [(sig.label, p) for sig in result.signatures for p in sig.parameters]
for (label, p) in params
@test p.label isa Tuple{Int,Int}
start, stop = p.label
@test 0 <= start <= stop
@test !occursin("#unused#", utf16_slice(label, p.label))
end
# An unnamed argument selects a leading `::Type`; `get` has such methods
# (e.g. `get(::Base.EnvDict, k, def)`), which used to be labeled `#unused#`.
@test any(((label, p),) -> startswith(utf16_slice(label, p.label), "::"), params)
end

@testitem "Signatures: function with var\"\" argument (#3867)" begin
using JuliaWorkspaces: JuliaWorkspace, add_file!, TextFile, SourceText, get_signature_help
using JuliaWorkspaces.URIs2: URI
Expand Down
Loading