Skip to content

Replace LanguageServer::Protocol::Transport with our own implementation #3533

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
11 changes: 10 additions & 1 deletion exe/ruby-lsp
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ if ENV["BUNDLE_GEMFILE"].nil?
exit exec(env, "#{base_command} exec ruby-lsp #{original_args.join(" ")}".strip)
end

$stdin.sync = true
$stdout.sync = true
$stderr.sync = true
$stdin.binmode
$stdout.binmode
$stderr.binmode

$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))

require "ruby_lsp/load_sorbet"
Expand Down Expand Up @@ -147,8 +154,10 @@ if options[:doctor]
return
end

server = RubyLsp::Server.new

# Ensure all output goes out stderr by default to allow puts/p/pp to work
# without specifying output device.
$> = $stderr

RubyLsp::Server.new.start
server.start
24 changes: 18 additions & 6 deletions exe/ruby-lsp-launcher
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@
# composed bundle
# !!!!!!!

$stdin.sync = true
$stdout.sync = true
$stderr.sync = true
$stdin.binmode
$stdout.binmode
$stderr.binmode

setup_error = nil
install_error = nil
reboot = false
Expand All @@ -28,7 +35,6 @@ else
# Read the initialize request before even starting the server. We need to do this to figure out the workspace URI.
# Editors are not required to spawn the language server process on the same directory as the workspace URI, so we need
# to ensure that we're setting up the bundle in the right place
$stdin.binmode
headers = $stdin.gets("\r\n\r\n")
content_length = headers[/Content-Length: (\d+)/i, 1].to_i
$stdin.read(content_length)
Expand Down Expand Up @@ -138,22 +144,28 @@ if ARGV.include?("--debug")
end
end

# Ensure all output goes out stderr by default to allow puts/p/pp to work without specifying output device.
$> = $stderr

initialize_request = JSON.parse(raw_initialize, symbolize_names: true) if raw_initialize

begin
RubyLsp::Server.new(
server = RubyLsp::Server.new(
install_error: install_error,
setup_error: setup_error,
initialize_request: initialize_request,
).start
)

# Ensure all output goes out stderr by default to allow puts/p/pp to work without specifying output device.
$> = $stderr

server.start
rescue ArgumentError
# If the launcher is booting an outdated version of the server, then the initializer doesn't accept a keyword splat
# and we already read the initialize request from the stdin pipe. In this case, we need to process the initialize
# request manually and then start the main loop
server = RubyLsp::Server.new

# Ensure all output goes out stderr by default to allow puts/p/pp to work without specifying output device.
$> = $stderr

server.process_message(initialize_request)
server.start
end
6 changes: 3 additions & 3 deletions lib/ruby_lsp/base_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ class BaseServer

#: (**untyped options) -> void
def initialize(**options)
@reader = MessageReader.new(options[:reader] || $stdin) #: MessageReader
@writer = MessageWriter.new(options[:writer] || $stdout) #: MessageWriter
@test_mode = options[:test_mode] #: bool?
@setup_error = options[:setup_error] #: StandardError?
@install_error = options[:install_error] #: StandardError?
@writer = Transport::Stdio::Writer.new #: Transport::Stdio::Writer
@reader = Transport::Stdio::Reader.new #: Transport::Stdio::Reader
@incoming_queue = Thread::Queue.new #: Thread::Queue
@outgoing_queue = Thread::Queue.new #: Thread::Queue
@cancelled_requests = [] #: Array[Integer]
Expand All @@ -40,7 +40,7 @@ def initialize(**options)

#: -> void
def start
@reader.read do |message|
@reader.each_message do |message|
method = message[:method]

# We must parse the document under a mutex lock or else we might switch threads and accept text edits in the
Expand Down
34 changes: 33 additions & 1 deletion lib/ruby_lsp/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ module RubyLsp
# rubocop:disable RubyLsp/UseLanguageServerAliases
Interface = LanguageServer::Protocol::Interface
Constant = LanguageServer::Protocol::Constant
Transport = LanguageServer::Protocol::Transport
# rubocop:enable RubyLsp/UseLanguageServerAliases

# Used to indicate that a request shouldn't return a response
Expand Down Expand Up @@ -302,4 +301,37 @@ def none? = @level == :none
#: -> bool
def true_or_higher? = @level == :true || @level == :strict
end

# Reads JSON RPC messages from the given IO in a loop
class MessageReader
#: (IO) -> void
def initialize(io)
@io = io
end

#: () { (Hash[Symbol, untyped]) -> void } -> void
def each_message(&block)
while (headers = @io.gets("\r\n\r\n"))
raw_message = @io.read(headers[/Content-Length: (\d+)/i, 1].to_i) #: as !nil
block.call(JSON.parse(raw_message, symbolize_names: true))
end
end
end

# Writes JSON RPC messages to the given IO
class MessageWriter
#: (IO) -> void
def initialize(io)
@io = io
end

#: (Hash[Symbol, untyped]) -> void
def write(message)
message[:jsonrpc] = "2.0"
json_message = message.to_json

@io.write("Content-Length: #{json_message.bytesize}\r\n\r\n#{json_message}")
@io.flush
end
end
end
17 changes: 17 additions & 0 deletions test/integration_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,23 @@ def test_launch_mode_retries_if_setup_failed_after_successful_install
end
end

def test_launching_an_older_server_version
in_temp_dir do |dir|
File.write(File.join(dir, "Gemfile"), <<~RUBY)
source "https://rubygems.org"
gem "ruby-lsp", "0.23.0"
RUBY

Bundler.with_unbundled_env do
capture_subprocess_io do
system("bundle", "install")
end

launch(dir)
end
end
end

private

def launch(workspace_path, exec = "ruby-lsp-launcher", extra_env = {})
Expand Down
Loading