diff --git a/lib/ruby_lsp/ruby_lsp_rails/runner_client.rb b/lib/ruby_lsp/ruby_lsp_rails/runner_client.rb index 53791fab..094307af 100644 --- a/lib/ruby_lsp/ruby_lsp_rails/runner_client.rb +++ b/lib/ruby_lsp/ruby_lsp_rails/runner_client.rb @@ -100,10 +100,16 @@ def initialize(outgoing_queue) end end - @logger_thread = T.let( + # Responsible for transmitting notifications coming from the server to the outgoing queue, so that we can do + # things such as showing progress notifications initiated by the server + @notifier_thread = T.let( Thread.new do - while (content = @stderr.gets("\n")) - log_message(content, type: RubyLsp::Constant::MessageType::LOG) + until @stderr.closed? + notification = read_notification + + unless @outgoing_queue.closed? || !notification + @outgoing_queue << notification + end end rescue IOError # The server was shutdown and stderr is already closed @@ -338,6 +344,21 @@ def read_content_length length.to_i end + + # Read a server notification from stderr. Only intended to be used by notifier thread + sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) } + def read_notification + headers = @stderr.gets("\r\n\r\n") + return unless headers + + length = headers[/Content-Length: (\d+)/i, 1] + return unless length + + raw_content = @stderr.read(length.to_i) + return unless raw_content + + JSON.parse(raw_content, symbolize_names: true) + end end class NullClient < RunnerClient diff --git a/lib/ruby_lsp/ruby_lsp_rails/server.rb b/lib/ruby_lsp/ruby_lsp_rails/server.rb index a205e0c3..2a9423f8 100644 --- a/lib/ruby_lsp/ruby_lsp_rails/server.rb +++ b/lib/ruby_lsp/ruby_lsp_rails/server.rb @@ -7,15 +7,9 @@ module RubyLsp module Rails module Common - # Write a message to the client. Can be used for sending notifications to the editor - def send_message(message) - json_message = message.to_json - @stdout.write("Content-Length: #{json_message.bytesize}\r\n\r\n#{json_message}") - end - # Log a message to the editor's output panel - def log_message(message) - $stderr.puts(message) + def log_message(message, type: 4) + send_notification({ method: "window/logMessage", params: { type: type, message: message } }) end # Sends an error result to a request, if the request failed. DO NOT INVOKE THIS METHOD FOR NOTIFICATIONS! Use @@ -54,6 +48,20 @@ def with_notification_error_handling(notification_name, &block) rescue => e log_message("Request #{notification_name} failed:\n#{e.full_message(highlight: false)}") end + + private + + # Write a response message back to the client + def send_message(message) + json_message = message.to_json + @stdout.write("Content-Length: #{json_message.bytesize}\r\n\r\n#{json_message}") + end + + # Write a notification to the client to be transmitted to the editor + def send_notification(message) + json_message = message.to_json + @stderr.write("Content-Length: #{json_message.bytesize}\r\n\r\n#{json_message}") + end end class ServerAddon @@ -76,16 +84,17 @@ def delegate(name, request, params) end # Instantiate all server addons and store them in a hash for easy access after we have discovered the classes - def finalize_registrations!(stdout) + def finalize_registrations!(stdout, stderr) until @server_addon_classes.empty? - addon = @server_addon_classes.shift.new(stdout) + addon = @server_addon_classes.shift.new(stdout, stderr) @server_addons[addon.name] = addon end end end - def initialize(stdout) + def initialize(stdout, stderr) @stdout = stdout + @stderr = stderr end def name @@ -100,11 +109,11 @@ def execute(request, params) class Server include Common - def initialize(stdout: $stdout, override_default_output_device: true) + def initialize(stdout: $stdout, stderr: $stderr, override_default_output_device: true) # Grab references to the original pipes so that we can change the default output device further down @stdin = $stdin @stdout = stdout - @stderr = $stderr + @stderr = stderr @stdin.sync = true @stdout.sync = true @stderr.sync = true @@ -169,7 +178,7 @@ def execute(request, params) when "server_addon/register" with_notification_error_handling(request) do require params[:server_addon_path] - ServerAddon.finalize_registrations!(@stdout) + ServerAddon.finalize_registrations!(@stdout, @stderr) end when "server_addon/delegate" server_addon_name = params[:server_addon_name] diff --git a/test/ruby_lsp_rails/runner_client_test.rb b/test/ruby_lsp_rails/runner_client_test.rb index 20720593..ea9f5b69 100644 --- a/test/ruby_lsp_rails/runner_client_test.rb +++ b/test/ruby_lsp_rails/runner_client_test.rb @@ -128,7 +128,7 @@ def name def execute(request, params) log_message("Hello!") - send_message({ request:, params: }) + send_result({ request: request, params: params }) end end RUBY @@ -141,16 +141,16 @@ def execute(request, params) # Finished booting server pop_log_notification(@outgoing_queue, RubyLsp::Constant::MessageType::LOG) - log = pop_log_notification(@outgoing_queue, RubyLsp::Constant::MessageType::LOG) + log = @outgoing_queue.pop # Sometimes we get warnings concerning deprecations and they mess up this expectation 3.times do - unless log.params.message.match?(/Hello!/) - log = pop_log_notification(@outgoing_queue, RubyLsp::Constant::MessageType::LOG) + unless log.dig(:params, :message).match?(/Hello!/) + log = @outgoing_queue.pop end end - assert_match("Hello!", log.params.message) + assert_match("Hello!", log.dig(:params, :message)) ensure FileUtils.rm("server_addon.rb") end diff --git a/test/ruby_lsp_rails/server_test.rb b/test/ruby_lsp_rails/server_test.rb index a6bc8cad..88e6a6e1 100644 --- a/test/ruby_lsp_rails/server_test.rb +++ b/test/ruby_lsp_rails/server_test.rb @@ -7,7 +7,8 @@ class ServerTest < ActiveSupport::TestCase setup do @stdout = StringIO.new - @server = RubyLsp::Rails::Server.new(stdout: @stdout, override_default_output_device: false) + @stderr = StringIO.new + @server = RubyLsp::Rails::Server.new(stdout: @stdout, stderr: @stderr, override_default_output_device: false) end test "returns nil if model doesn't exist" do @@ -229,6 +230,23 @@ def resolve_route_info(requirements) assert_equal expected, @stdout.string end + test "log_message sends notification to client" do + @server.log_message("Hello") + expected_notification = { method: "window/logMessage", params: { type: 4, message: "Hello" } }.to_json + assert_equal "Content-Length: #{expected_notification.bytesize}\r\n\r\n#{expected_notification}", @stderr.string + end + + test "log_message allows server to define message type" do + @server.log_message("Hello", type: 1) + + expected_notification = { + method: "window/logMessage", + params: { type: 1, message: "Hello" }, + }.to_json + + assert_equal "Content-Length: #{expected_notification.bytesize}\r\n\r\n#{expected_notification}", @stderr.string + end + private def response