Skip to content

Commit 6de4e8d

Browse files
authored
Replace LanguageServer::Protocol::Transport with our own implementation (#3533)
1 parent d0d3736 commit 6de4e8d

File tree

5 files changed

+81
-11
lines changed

5 files changed

+81
-11
lines changed

exe/ruby-lsp

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ if ENV["BUNDLE_GEMFILE"].nil?
8888
exit exec(env, "#{base_command} exec ruby-lsp #{original_args.join(" ")}".strip)
8989
end
9090

91+
$stdin.sync = true
92+
$stdout.sync = true
93+
$stderr.sync = true
94+
$stdin.binmode
95+
$stdout.binmode
96+
$stderr.binmode
97+
9198
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
9299

93100
require "ruby_lsp/internal"
@@ -144,8 +151,10 @@ if options[:doctor]
144151
return
145152
end
146153

154+
server = RubyLsp::Server.new
155+
147156
# Ensure all output goes out stderr by default to allow puts/p/pp to work
148157
# without specifying output device.
149158
$> = $stderr
150159

151-
RubyLsp::Server.new.start
160+
server.start

exe/ruby-lsp-launcher

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66
# composed bundle
77
# !!!!!!!
88

9+
$stdin.sync = true
10+
$stdout.sync = true
11+
$stderr.sync = true
12+
$stdin.binmode
13+
$stdout.binmode
14+
$stderr.binmode
15+
916
setup_error = nil
1017
install_error = nil
1118
reboot = false
@@ -28,7 +35,6 @@ else
2835
# Read the initialize request before even starting the server. We need to do this to figure out the workspace URI.
2936
# Editors are not required to spawn the language server process on the same directory as the workspace URI, so we need
3037
# to ensure that we're setting up the bundle in the right place
31-
$stdin.binmode
3238
headers = $stdin.gets("\r\n\r\n")
3339
content_length = headers[/Content-Length: (\d+)/i, 1].to_i
3440
$stdin.read(content_length)
@@ -140,22 +146,28 @@ if ARGV.include?("--debug")
140146
end
141147
end
142148

143-
# Ensure all output goes out stderr by default to allow puts/p/pp to work without specifying output device.
144-
$> = $stderr
145-
146149
initialize_request = JSON.parse(raw_initialize, symbolize_names: true) if raw_initialize
147150

148151
begin
149-
RubyLsp::Server.new(
152+
server = RubyLsp::Server.new(
150153
install_error: install_error,
151154
setup_error: setup_error,
152155
initialize_request: initialize_request,
153-
).start
156+
)
157+
158+
# Ensure all output goes out stderr by default to allow puts/p/pp to work without specifying output device.
159+
$> = $stderr
160+
161+
server.start
154162
rescue ArgumentError
155163
# If the launcher is booting an outdated version of the server, then the initializer doesn't accept a keyword splat
156164
# and we already read the initialize request from the stdin pipe. In this case, we need to process the initialize
157165
# request manually and then start the main loop
158166
server = RubyLsp::Server.new
167+
168+
# Ensure all output goes out stderr by default to allow puts/p/pp to work without specifying output device.
169+
$> = $stderr
170+
159171
server.process_message(initialize_request)
160172
server.start
161173
end

lib/ruby_lsp/base_server.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ module RubyLsp
66
class BaseServer
77
#: (**untyped options) -> void
88
def initialize(**options)
9+
@reader = MessageReader.new(options[:reader] || $stdin) #: MessageReader
10+
@writer = MessageWriter.new(options[:writer] || $stdout) #: MessageWriter
911
@test_mode = options[:test_mode] #: bool?
1012
@setup_error = options[:setup_error] #: StandardError?
1113
@install_error = options[:install_error] #: StandardError?
12-
@writer = Transport::Stdio::Writer.new #: Transport::Stdio::Writer
13-
@reader = Transport::Stdio::Reader.new #: Transport::Stdio::Reader
1414
@incoming_queue = Thread::Queue.new #: Thread::Queue
1515
@outgoing_queue = Thread::Queue.new #: Thread::Queue
1616
@cancelled_requests = [] #: Array[Integer]
@@ -36,7 +36,7 @@ def initialize(**options)
3636

3737
#: -> void
3838
def start
39-
@reader.read do |message|
39+
@reader.each_message do |message|
4040
method = message[:method]
4141

4242
# We must parse the document under a mutex lock or else we might switch threads and accept text edits in the

lib/ruby_lsp/utils.rb

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ module RubyLsp
55
# rubocop:disable RubyLsp/UseLanguageServerAliases
66
Interface = LanguageServer::Protocol::Interface
77
Constant = LanguageServer::Protocol::Constant
8-
Transport = LanguageServer::Protocol::Transport
98
# rubocop:enable RubyLsp/UseLanguageServerAliases
109

1110
# Used to indicate that a request shouldn't return a response
@@ -306,4 +305,37 @@ def none? = @level == :none
306305
#: -> bool
307306
def true_or_higher? = @level == :true || @level == :strict
308307
end
308+
309+
# Reads JSON RPC messages from the given IO in a loop
310+
class MessageReader
311+
#: (IO) -> void
312+
def initialize(io)
313+
@io = io
314+
end
315+
316+
#: () { (Hash[Symbol, untyped]) -> void } -> void
317+
def each_message(&block)
318+
while (headers = @io.gets("\r\n\r\n"))
319+
raw_message = @io.read(headers[/Content-Length: (\d+)/i, 1].to_i) #: as !nil
320+
block.call(JSON.parse(raw_message, symbolize_names: true))
321+
end
322+
end
323+
end
324+
325+
# Writes JSON RPC messages to the given IO
326+
class MessageWriter
327+
#: (IO) -> void
328+
def initialize(io)
329+
@io = io
330+
end
331+
332+
#: (Hash[Symbol, untyped]) -> void
333+
def write(message)
334+
message[:jsonrpc] = "2.0"
335+
json_message = message.to_json
336+
337+
@io.write("Content-Length: #{json_message.bytesize}\r\n\r\n#{json_message}")
338+
@io.flush
339+
end
340+
end
309341
end

test/integration_test.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,23 @@ def test_launch_mode_retries_if_setup_failed_after_successful_install
333333
end
334334
end
335335

336+
def test_launching_an_older_server_version
337+
in_temp_dir do |dir|
338+
File.write(File.join(dir, "Gemfile"), <<~RUBY)
339+
source "https://rubygems.org"
340+
gem "ruby-lsp", "0.23.0"
341+
RUBY
342+
343+
Bundler.with_unbundled_env do
344+
capture_subprocess_io do
345+
system("bundle", "install")
346+
end
347+
348+
launch(dir)
349+
end
350+
end
351+
end
352+
336353
private
337354

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

0 commit comments

Comments
 (0)