diff --git a/Project.toml b/Project.toml index f52a5cd57..560542fc0 100644 --- a/Project.toml +++ b/Project.toml @@ -17,14 +17,15 @@ Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" Preferences = "21216c6a-2e73-6563-6e65-726566657250" REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" +Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76" [sources] JET = {rev = "master", url = "https://github.com/aviatesk/JET.jl"} -JSONRPC = {path = "JSONRPC"} +JSONRPC = {subdir = "JSONRPC", url = "https://github.com/aviatesk/JETLS.jl"} JuliaLowering = {rev = "jetls-hacking-2", url = "https://github.com/mlechu/JuliaLowering.jl"} JuliaSyntax = {rev = "jetls-hacking-2", url = "https://github.com/JuliaLang/JuliaSyntax.jl"} -LSP = {path = "LSP"} +LSP = {subdir = "LSP", url = "https://github.com/aviatesk/JETLS.jl"} [compat] JET = "0.10.6" @@ -37,5 +38,10 @@ Pkg = "1.11.0" PrecompileTools = "1.3.2" Preferences = "1.4.3" REPL = "1.11.0" +Sockets = "1.11.0" TOML = "1.0.3" julia = "1.12" + +[apps.jetls] +submodule = "CLI" +julia_flags = ["--startup-file=no", "--history-file=no", "--threads=auto"] diff --git a/src/CLI.jl b/src/CLI.jl new file mode 100644 index 000000000..16755c47b --- /dev/null +++ b/src/CLI.jl @@ -0,0 +1,176 @@ +module CLI + +@info "Running JETLS with Julia version" VERSION + +using Pkg +using Sockets + +# TODO load Revise only when `JETLS_DEV_MODE` is true +try + # load Revise with JuliaInterpreter used by JETLS + using Revise +catch + @warn "Revise not found" +end + +@info "Loading JETLS..." + +try + using JETLS +catch + @error "JETLS not found in this environment" Pkg.project().path + exit(1) +end + +function show_help() + println(stdout, """ + JETLS - A Julia language server providing advanced static analysis and seamless + runtime integration. Powered by JET.jl, JuliaSyntax.jl, and JuliaLowering.jl. + + Usage: julia runserver.jl [OPTIONS] + + Communication channel options (choose one, default: --stdio): + --stdio Use standard input/output + --pipe= Use named pipe (Windows) or Unix domain socket + --socket= Use TCP socket on specified port + + Options: + --clientProcessId= Monitor client process (server shuts down if client exits) + --help, -h Show this help message + + Examples: + julia runserver.jl + julia runserver.jl --socket=8080 + julia runserver.jl --pipe=/tmp/jetls.sock --clientProcessId=12345 + """) +end + +function (@main)(args::Vector{String})::Cint + pipe_name = socket_port = client_process_id = nothing + help_requested = false + + i = 1 + while i <= length(args) + arg = args[i] + if occursin(r"^(?:-h|--help|help)$", arg) + show_help() + return Cint(0) + elseif occursin(r"^(?:--)?stdio$", arg) + elseif occursin(r"^(?:--)?pipe$", arg) + socket_port = nothing + if i < length(args) + pipe_name = args[i+1] + i += 1 + else + @error "--pipe requires a path argument: use --pipe= or --pipe " + return Cint(1) + end + elseif (m = match(r"^--pipe=(.+)$", arg); !isnothing(m)) + pipe_name = m.captures[1] + elseif occursin(r"^(?:--)?socket$", arg) + if i < length(args) + socket_port = tryparse(Int, args[i+1]) + i += 1 + @goto check_socket_port + else + @error "--socket requires a port argument: use --socket= or --socket " + return Cint(1) + end + elseif (m = match(r"^--socket=(\d+)$", arg); !isnothing(m)) + socket_port = tryparse(Int, m.captures[1]) + @label check_socket_port + if isnothing(socket_port) + @error "Invalid port number for --socket (must be a valid integer)" + return Cint(1) + end + elseif occursin(r"^--clientProcessId$", arg) + if i < length(args) + client_process_id = tryparse(Int, args[i+1]) + i += 1 + @goto check_client_process_id + else + @error "--clientProcessId requires a process ID argument: use --clientProcessId= or --clientProcessId " + return Cint(1) + end + elseif (m = match(r"^--clientProcessId=(\d+)$", arg); !isnothing(m)) + client_process_id = tryparse(Int, m.captures[1]) + @label check_client_process_id + if isnothing(client_process_id) + @error "Invalid process ID for --clientProcessId (must be a valid integer)" + return Cint(1) + end + else + @error "Unknown CLI argument" arg + return Cint(1) + end + i += 1 + end + + isnothing(client_process_id) || + @info "Client process ID provided via command line" client_process_id + + # Create endpoint based on communication channel + if !isnothing(pipe_name) + # Try to connect to client-created socket first, then fallback to creating our own + try + pipe_type = Sys.iswindows() ? "Windows named pipe" : "Unix domain socket" + # Most LSP clients expect server to create the socket, but VSCode extension creates it + # Try connecting first (for VSCode), fallback to listen/accept (for other clients). + try + conn = connect(pipe_name) + endpoint = LSEndpoint(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) + @info "Accepted connection on $pipe_type" + end + catch e + @error "Failed to create pipe/socket connection" pipe_name + Base.display_error(stderr, e, catch_backtrace()) + return Cint(1) + end + elseif !isnothing(socket_port) + try + server_socket = listen(socket_port) + actual_port = getsockname(server_socket)[2] + println(stdout, "$actual_port") + @info "Waiting for connection on port" actual_port + conn = accept(server_socket) + endpoint = LSEndpoint(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) + @info "Using stdio for communication" + end + + if JETLS.JETLS_DEV_MODE + server = Server(endpoint) do s::Symbol, x + @nospecialize x + # allow Revise to apply changes with the dev mode enabled + if s === :received + if !(x isa JETLS.ShutdownRequest || x isa JETLS.ExitNotification) + Revise.revise() + end + end + end + JETLS.currently_running = server + t = Threads.@spawn :interactive runserver(server) + else + t = Threads.@spawn :interactive runserver(endpoint) + end + res = fetch(t) + @info "JETLS server stopped" res.exit_code + return res.exit_code +end + +end # module CLI diff --git a/src/JETLS.jl b/src/JETLS.jl index f80ab6d11..0c6c7e605 100644 --- a/src/JETLS.jl +++ b/src/JETLS.jl @@ -388,4 +388,6 @@ end include("precompile.jl") +include("CLI.jl") + end # module JETLS