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.

2 changes: 2 additions & 0 deletions LSP/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ version = "0.1.0"
authors = ["Shuhei Kadowaki <aviatesk@gmail.com>"]

[deps]
JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1"
StructTypes = "856f2bd8-1eba-4b0a-8007-ebc267875bd4"

[compat]
JSON3 = "1.14.3"
StructTypes = "1.11.0"
83 changes: 80 additions & 3 deletions LSP/src/utils/interface.jl → LSP/src/DSL/interface.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,82 @@
const _INTERFACE_DEFS = Dict{Symbol,Expr}()
const _interface_defs_ = Dict{Symbol,Expr}()

"""
@interface InterfaceName [@extends ParentInterface] begin
field::Type
optionalField::Union{Nothing, Type} = nothing
...
end

Creates a Julia struct with keyword constructor that mirrors TypeScript interface
definitions from the LSP specification, featuring:
- Keyword constructor: All structs are created with `@kwdef`, enabling keyword-based construction
- Optional fields: Use `Union{Nothing, Type} = nothing` to represent TypeScript's optional properties (`field?: Type`)
- Interface inheritance: Use `@extends` to compose interfaces from parent interfaces (similar to TypeScript's `extends`)
- Anonymous interfaces: Can be used inline within field type declarations (e.g., `Union{Nothing, @interface begin ... end}`)
- Method dispatching: For interfaces extending `RequestMessage` or `NotificationMessage`,
automatically registers the message type in the `method_dispatcher` dictionary for LSP message routing

# Field declarations

Fields can be declared with type annotations and optional default values:

```julia
@interface Example begin
"Required field"
requiredField::String

"Optional field that can be omitted"
optionalField::Union{Nothing, Bool} = nothing

"Field with default value"
fieldWithDefault::Int = 0
end
```

# Anonymous interfaces

Anonymous interfaces can be used within field type declarations for inline type specifications:

```julia
@interface Outer begin
nested::Union{Nothing, @interface begin
innerField::String
end} = nothing
end
```

# Inheritance

The `@extends` syntax allows composing interfaces from one or more parent interfaces:

```julia
@interface Child @extends Parent begin
childField::String
end

@interface MultipleInheritance @extends (Parent1, Parent2) begin
additionalField::Int
end
```

When a child interface defines a field with the same name as a parent interface, the child's definition takes precedence.

# LSP message dispatching

For interfaces that extend `RequestMessage` or `NotificationMessage`, a `method::String`
field must be defined:
```julia
@interface MyRequest @extends RequestMessage begin
method::String = "textDocument/myRequest"
params::MyParams
end
```

This automatically registers the interface in `method_dispatcher["textDocument/myRequest"]`,
enabling routing of incoming LSP messages to the appropriate handler.

See also: [`@namespace`](@ref)
"""
macro interface(exs...)
nexs = length(exs)
if nexs == 1
Expand Down Expand Up @@ -101,7 +178,7 @@ function process_interface_def!(toplevelblk::Expr, structbody::Expr,
push!(toplevelblk.args, :(Base.convert(::Type{$Name}, nt::NamedTuple) = $Name(; nt...)))
end
if !is_anon
push!(toplevelblk.args, :($(GlobalRef(@__MODULE__, :_INTERFACE_DEFS))[$(QuoteNode(Name))] = $(QuoteNode(structbody))))
push!(toplevelblk.args, :($(GlobalRef(@__MODULE__, :_interface_defs_))[$(QuoteNode(Name))] = $(QuoteNode(structbody))))
end
return Name, method
end
Expand All @@ -116,7 +193,7 @@ function add_extended_interface!(toplevelblk::Expr, structbody::Expr,
omittable_fields,
extended_fields,
duplicated_fields,
_INTERFACE_DEFS[extend],
_interface_defs_[extend],
__source__;
extending = true)
end
Expand Down
42 changes: 42 additions & 0 deletions LSP/src/utils/namespace.jl → LSP/src/DSL/namespace.jl
Original file line number Diff line number Diff line change
@@ -1,3 +1,45 @@
"""
@namespace NamespaceName::Type begin
CONSTANT_NAME = value
...
end

Creates a Julia module containing typed constants that correspond to TypeScript `namespace`
definitions from the LSP specification.

# Type references

Due to the design that mimics TypeScript `namespaces` using Julia's module system,
namespace types must be referenced with the `.Ty` suffix:

```julia
@namespace SignatureHelpTriggerKind::Int begin
Invoked = 1
TriggerCharacter = 2
ContentChange = 3
end

@interface SignatureHelpContext begin
...
# Use as type annotation
triggerKind::SignatureHelpTriggerKind.Ty
...
end
```

This is a constraint of Julia's module scoping rules, where constants and type aliases
within modules cannot be accessed without explicit qualification.

# Implementation details

The macro generates:
1. A Julia module with the specified namespace name
2. Constants with the specified type and values
3. A `Ty` type alias equal to the specified type for convenient type references
4. Automatic export of the namespace name to the parent module

See also: [`@interface`](@ref)
"""
macro namespace(exs...)
nexs = length(exs)
nexs == 2 || error("`@namespace` expected 2 arguments: ", exs)
Expand Down
13 changes: 8 additions & 5 deletions LSP/src/LSP.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ using ..URIs2: URI
const exports = Set{Symbol}()
const method_dispatcher = Dict{String,DataType}()

include("utils/interface.jl")
include("utils/namespace.jl")
include("DSL/interface.jl")
include("DSL/namespace.jl")

include("base-protocol.jl")
include("basic-json-structures.jl")
Expand Down Expand Up @@ -40,11 +40,14 @@ include("workspace-features/apply-edit.jl")
include("window-features.jl")
include("lifecycle-messages/initialize.jl")

include("communication.jl")
module Communication
using ..LSP: Endpoint, send
export Endpoint, send
end

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

export
method_dispatcher

end # module LSP
90 changes: 65 additions & 25 deletions JSONRPC/src/JSONRPC.jl → LSP/src/communication.jl
Original file line number Diff line number Diff line change
@@ -1,17 +1,48 @@
module JSONRPC
using JSON3: JSON3

export Endpoint, send
"""
Endpoint

using JSON3
A bidirectional communication endpoint for Language Server Protocol messages.

`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 +53,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 +65,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,21 +81,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

const Parsed = @NamedTuple{method::Union{Nothing,String}}

function readmsg(io::IO, method_dispatcher)
msg_str = @something read_transport_layer(io) return nothing
parsed = JSON3.read(msg_str, Parsed)
return reparse_msg_str(parsed, msg_str, method_dispatcher)
end
readlsp(io::IO) = to_lsp_object(@something read_transport_layer(io) return nothing)

function read_transport_layer(io::IO)
line = chomp(readline(io))
Expand All @@ -84,22 +109,22 @@ function read_transport_layer(io::IO)
return String(read(io, message_length))
end

function reparse_msg_str(parsed::Parsed, msg_str::String, method_dispatcher)
const Parsed = @NamedTuple{method::Union{Nothing,String}}

function to_lsp_object(msg_str::AbstractString)
parsed = JSON3.read(msg_str, Parsed)
parsed_method = parsed.method
if parsed_method !== nothing
if haskey(method_dispatcher, parsed_method)
return JSON3.read(msg_str, method_dispatcher[parsed_method])
end
return JSON3.read(msg_str, Dict{Symbol,Any})
else # TODO parse to ResponseMessage?
return JSON3.read(msg_str, Dict{Symbol,Any})
end
# TODO Parse response message?
return JSON3.read(msg_str, Dict{Symbol,Any})
end

function writemsg(io::IO, @nospecialize msg)
msg_str = JSON3.write(msg)
write_transport_layer(io, msg_str)
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)
Expand All @@ -110,6 +135,8 @@ function write_transport_layer(io::IO, response::String)
return n
end

to_lsp_json(@nospecialize msg) = JSON3.write(msg)

function Base.close(endpoint::Endpoint)
flush(endpoint)
close(endpoint.in_msg_queue)
Expand Down Expand Up @@ -139,10 +166,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
1 change: 0 additions & 1 deletion LSP/src/language-features/completions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,6 @@ end
export CompletionData

@interface CompletionItem begin

"""
The label of this completion item.

Expand Down
2 changes: 1 addition & 1 deletion LSP/src/language-features/definition.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ end

@interface DefinitionResponse @extends ResponseMessage begin
result::Union{Location, Vector{Location}, Vector{LocationLink}, Null, Nothing}
end
end
Loading
Loading