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
1 change: 0 additions & 1 deletion JSONRPC/.gitignore

This file was deleted.

10 changes: 0 additions & 10 deletions JSONRPC/Project.toml

This file was deleted.

14 changes: 9 additions & 5 deletions LSP/src/LSP.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ include("URIs2/URIs2.jl")
using ..URIs2: URI

const exports = Set{Symbol}()

const method_dispatcher = Dict{String,DataType}()

# NOTE `Null` and `URI` are referenced directly from interface.jl, so it should be defined before that.
Expand Down Expand Up @@ -58,13 +59,16 @@ include("workspace-features/apply-edit.jl")
include("window-features.jl")
include("lifecycle-messages/initialize.jl")

for name in exports
Core.eval(@__MODULE__, Expr(:export, name))
include("communication.jl")
module Communication
using ..LSP: Endpoint, send
export Endpoint, send
end

export
method_dispatcher

include("precompile.jl")

for name in exports
Core.eval(@__MODULE__, Expr(:export, name))
end

end # module LSP
96 changes: 67 additions & 29 deletions JSONRPC/src/JSONRPC.jl → LSP/src/communication.jl
Original file line number Diff line number Diff line change
@@ -1,17 +1,46 @@
module JSONRPC
"""
Endpoint

export Endpoint, send
A bidirectional communication endpoint for Language Server Protocol messages.

using JSON
`Endpoint` manages asynchronous reading and writing of LSP messages over IO streams.
It spawns two separate tasks:
- A read task that continuously reads messages from the input stream and queues them
- A write task that continuously writes messages from the output queue to the output stream

Both tasks run on the `:interactive` thread pool to ensure responsive message handling.

- `in_msg_queue::Channel{Any}`: Queue of incoming messages read from the input stream
- `out_msg_queue::Channel{Any}`: Queue of outgoing messages to be written to the output stream
- `read_task::Task`: Task handling message reading
- `write_task::Task`: Task handling message writing
- `isopen::Bool`: Atomic flag indicating whether the endpoint is open

There are two constructors:
- `Endpoint(in::IO, out::IO)`
- `Endpoint(err_handler, in::IO, out::IO)`

The later creates an endpoint with custom error handler or default error handler that logs to `stderr`.
The error handler should have signature `(isread::Bool, err, backtrace) -> nothing`.

# Example
```julia
endpoint = Endpoint(stdin, stdout)
for msg in endpoint
# Process incoming messages
send(endpoint, response)
end
close(endpoint)
```
"""
mutable struct Endpoint
in_msg_queue::Channel{Any}
out_msg_queue::Channel{Any}
read_task::Task
write_task::Task
@atomic isopen::Bool

function Endpoint(err_handler, in::IO, out::IO, method_dispatcher)
function Endpoint(err_handler, in::IO, out::IO)
in_msg_queue = Channel{Any}(Inf)
out_msg_queue = Channel{Any}(Inf)

Expand All @@ -22,7 +51,7 @@ mutable struct Endpoint
break
end
msg = @something try
readmsg(in, method_dispatcher)
readlsp(in)
catch err
err_handler(#=isread=#true, err, catch_backtrace())
continue
Expand All @@ -34,7 +63,7 @@ mutable struct Endpoint
write_task = Threads.@spawn :interactive for msg in out_msg_queue
if isopen(out)
try
writemsg(out, msg)
writelsp(out, msg)
catch err
err_handler(#=isread=#false, err, catch_backtrace())
continue
Expand All @@ -50,28 +79,15 @@ mutable struct Endpoint
end
end

function Endpoint(in::IO, out::IO, method_dispatcher)
Endpoint(in, out, method_dispatcher) do isread::Bool, err, bt
function Endpoint(in::IO, out::IO)
Endpoint(in, out) do isread::Bool, err, bt
@nospecialize err
@error "Error in Endpoint $(isread ? "reading" : "writing") task"
Base.display_error(stderr, err, bt)
end
end

function readmsg(io::IO, method_dispatcher)
msg_str = @something read_transport_layer(io) return nothing
lazyjson = JSON.lazy(msg_str)
if hasproperty(lazyjson, :method)
method = lazyjson.method[]
if method isa String && haskey(method_dispatcher, method)
return JSON.parse(lazyjson, method_dispatcher[method])
end
return JSON.parse(lazyjson, Dict{Symbol,Any})
else # TODO parse to ResponseMessage?
return JSON.parse(lazyjson, Dict{Symbol,Any})
end
end

readlsp(io::IO) = to_lsp_object(@something read_transport_layer(io) return nothing)
function read_transport_layer(io::IO)
line = chomp(readline(io))
if line == ""
Expand All @@ -89,12 +105,20 @@ function read_transport_layer(io::IO)
message_length = parse(Int, var"Content-Length")
return String(read(io, message_length))
end

function writemsg(io::IO, @nospecialize msg)
msg_str = JSON.json(msg; omit_null=true)
write_transport_layer(io, msg_str)
function to_lsp_object(msg_str::AbstractString)
lazyjson = JSON.lazy(msg_str)
if hasproperty(lazyjson, :method)
method = lazyjson.method[]
if method isa String && haskey(method_dispatcher, method)
return JSON.parse(lazyjson, method_dispatcher[method])
end
return JSON.parse(lazyjson, Dict{Symbol,Any})
end
# TODO Parse response messages?
return JSON.parse(lazyjson, Dict{Symbol,Any})
end

writelsp(io::IO, @nospecialize msg) = write_transport_layer(io, to_lsp_json(msg))
function write_transport_layer(io::IO, response::String)
response_utf8 = transcode(UInt8, response)
n = length(response_utf8)
Expand All @@ -103,6 +127,7 @@ function write_transport_layer(io::IO, response::String)
flush(io)
return n
end
to_lsp_json(@nospecialize msg) = JSON.json(msg; omit_null=true)

function Base.close(endpoint::Endpoint)
flush(endpoint)
Expand Down Expand Up @@ -133,10 +158,23 @@ function Base.iterate(endpoint::Endpoint, _=nothing)
return take!(endpoint.in_msg_queue), nothing
end

"""
send(endpoint::Endpoint, msg)

Send a message through the endpoint's output queue.

The message will be asynchronously written to the output stream by the endpoint's write task.
This function is non-blocking and returns immediately after queueing the message.

# Arguments
- `endpoint::Endpoint`: The endpoint to send the message through
- `msg`: The message to send (typically an LSP message structure)

# Throws
- `ErrorException`: If the endpoint is closed
"""
function send(endpoint::Endpoint, @nospecialize(msg::Any))
check_dead_endpoint!(endpoint)
put!(endpoint.out_msg_queue, msg)
return msg
nothing
end

end # module JSONRPC
31 changes: 13 additions & 18 deletions LSP/src/precompile.jl
Original file line number Diff line number Diff line change
@@ -1,33 +1,28 @@
function readlsp(msg_str::AbstractString)
lazyjson = JSON.lazy(msg_str)
if hasproperty(lazyjson, :method)
method = lazyjson.method[]
if haskey(method_dispatcher, method)
return JSON.parse(lazyjson, method_dispatcher[method])
end
return JSON.parse(lazyjson, Dict{Symbol,Any})
else # TODO parse to ResponseMessage?
return JSON.parse(lazyjson, Dict{Symbol,Any})
end
end
writelsp(x) = JSON.json(x; omit_null=true)

function test_roundtrip(f, s::AbstractString, Typ)
x = JSON.parse(s, Typ)
f(x)
s′ = writelsp(x)
s′ = to_lsp_json(x)
x′ = JSON.parse(s′, Typ)
f(x′)
end

function test_roundtrip(f, s::AbstractString)
x = to_lsp_object(s)
f(x)
s′ = to_lsp_json(x)
x′ = to_lsp_object(s′)
f(x′)
end

using PrecompileTools

@setup_workload let
uri = LSP.URIs2.filepath2uri(abspath(@__FILE__))
@compile_workload let
test_roundtrip("""{
"jsonrpc": "2.0",
"id":0, "method":"textDocument/completion",
"id": 0,
"method": "textDocument/completion",
"params": {
"textDocument": {
"uri": "$uri"
Expand All @@ -39,7 +34,7 @@ using PrecompileTools
"workDoneToken": "workDoneToken",
"partialResultToken": "partialResultToken"
}
}""", CompletionRequest) do req
}""") do req
@assert req isa CompletionRequest
end
test_roundtrip("""{
Expand All @@ -62,7 +57,7 @@ using PrecompileTools
"newText": "newText"
}
}
}""", CompletionResolveRequest) do req
}""") do req
@assert req isa CompletionResolveRequest
@assert req.params.textEdit isa TextEdit
end
Expand Down
16 changes: 5 additions & 11 deletions LSP/test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using LSP
using LSP.URIs2
using LSP: readlsp, writelsp, test_roundtrip
using LSP: to_lsp_object, to_lsp_json, test_roundtrip
using JSON
using Test

Expand Down Expand Up @@ -189,7 +189,7 @@ end
@test req.params.documentation isa MarkupContent
end

let init_req_s = """
test_roundtrip("""
{
"jsonrpc": "2.0",
"id": 0,
Expand All @@ -204,8 +204,7 @@ end
"workspaceFolders": []
}
}
"""
init_req = readlsp(init_req_s)
""") do init_req
@test init_req isa InitializeRequest
@test init_req.jsonrpc == "2.0"
@test init_req.id == 0
Expand All @@ -215,19 +214,14 @@ end
@test init_req.params.clientInfo.version == "1.0"
@test init_req.params.capabilities == ClientCapabilities()
@test init_req.params.workspaceFolders isa Vector{WorkspaceFolder} && isempty(init_req.params.workspaceFolders)

init_req_s′ = writelsp(init_req)
init_req′ = readlsp(init_req_s′)
@test init_req′ isa InitializeRequest
@test init_req′.params.workspaceFolders isa Vector{WorkspaceFolder} && isempty(init_req.params.workspaceFolders)
end

# ResponseMessage should omit the `error` field on success, and omit `result` an error
@testset "ResponseMessage result field" begin
success_res = ResponseMessage(;
id = "id",
result = null)
success_res_s = writelsp(success_res)
success_res_s = to_lsp_json(success_res)
@test occursin("\"result\"", success_res_s) && occursin("null", success_res_s)
@test !occursin("\"error\"", success_res_s)
end
Expand All @@ -239,7 +233,7 @@ end
code = ErrorCodes.RequestFailed,
message = "test message",
data = :test_data))
error_res_s = writelsp(error_res)
error_res_s = to_lsp_json(error_res)
@test !occursin("\"result\"", error_res_s)
@test occursin("\"error\"", error_res_s)
end
Expand Down
3 changes: 0 additions & 3 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ projects = ["docs", "test"]
[deps]
Configurations = "5218b696-f38b-4ac9-8b61-a12ec717816d"
JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b"
JSONRPC = "a2756949-8476-49a1-a294-231eace0f283"
JuliaLowering = "f3c80556-a63f-4383-b822-37d64f81a311"
JuliaSyntax = "70703baa-626e-46a2-a12c-08ffd08c73b4"
LSP = "880dcf91-6fde-4251-87fc-bfd84012291a"
Expand All @@ -22,15 +21,13 @@ TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76"

[sources]
JET = {rev = "1e84376", url = "https://github.com/aviatesk/JET.jl"}
JSONRPC = {path = "JSONRPC"}
JuliaLowering = {rev = "avi/JETLS-JSJL-head", url = "https://github.com/JuliaLang/julia", subdir="JuliaLowering"}
JuliaSyntax = {rev = "avi/JETLS-JSJL-head", url = "https://github.com/JuliaLang/julia", subdir="JuliaSyntax"}
LSP = {path = "LSP"}

[compat]
Configurations = "0.17.6"
JET = "0.10.6"
JSONRPC = "0.1"
JuliaLowering = "1"
JuliaSyntax = "2"
LSP = "0.1"
Expand Down
8 changes: 4 additions & 4 deletions runserver.jl
Original file line number Diff line number Diff line change
Expand Up @@ -118,15 +118,15 @@ function (@main)(args::Vector{String})::Cint
# Try connecting first (for VSCode), fallback to listen/accept (for other clients).
try
conn = connect(pipe_name)
endpoint = LSEndpoint(conn, conn)
endpoint = Endpoint(conn, conn)
@info "Connected to existing $pipe_type" pipe_name
catch
# Connection failed - client expects us to create the socket
@info "No existing socket found, creating server socket: $pipe_name"
server_socket = listen(pipe_name)
@info "Waiting for connection on $pipe_type: $pipe_name"
conn = accept(server_socket)
endpoint = LSEndpoint(conn, conn)
endpoint = Endpoint(conn, conn)
@info "Accepted connection on $pipe_type"
end
catch e
Expand All @@ -141,15 +141,15 @@ function (@main)(args::Vector{String})::Cint
println(stdout, "<JETLS-PORT>$actual_port</JETLS-PORT>")
@info "Waiting for connection on port" actual_port
conn = accept(server_socket)
endpoint = LSEndpoint(conn, conn)
endpoint = Endpoint(conn, conn)
@info "Connected via TCP socket" actual_port
catch e
@error "Failed to create socket connection" socket_port
Base.display_error(stderr, e, catch_backtrace())
return Cint(1)
end
else # use stdio as the communication channel
endpoint = LSEndpoint(stdin, stdout)
endpoint = Endpoint(stdin, stdout)
@info "Using stdio for communication"
end

Expand Down
Loading
Loading