diff --git a/lib/ruby_lsp/ruby_lsp_rails/server.rb b/lib/ruby_lsp/ruby_lsp_rails/server.rb index 4381b44c..702b60bc 100644 --- a/lib/ruby_lsp/ruby_lsp_rails/server.rb +++ b/lib/ruby_lsp/ruby_lsp_rails/server.rb @@ -7,6 +7,33 @@ module RubyLsp module Rails module Common + class Progress + def initialize(stderr, id, supports_progress) + @stderr = stderr + @id = id + @supports_progress = supports_progress + end + + def report(percentage: nil, message: nil) + return unless @supports_progress + return unless percentage || message + + json_message = { + method: "$/progress", + params: { + token: @id, + value: { + kind: "report", + percentage: percentage, + message: message, + }, + }, + }.to_json + + @stderr.write("Content-Length: #{json_message.bytesize}\r\n\r\n#{json_message}") + end + end + # Log a message to the editor's output panel def log_message(message, type: 4) send_notification({ method: "window/logMessage", params: { type: type, message: message } }) @@ -49,6 +76,68 @@ def with_notification_error_handling(notification_name, &block) log_message("Request #{notification_name} failed:\n#{e.full_message(highlight: false)}") end + def begin_progress(id, title, percentage: nil, message: nil) + return unless @capabilities[:supports_progress] + + # This is actually a request, but it is sent asynchronously and we do not return the response back to the + # server, so we consider it a notification from the perspective of the client/runtime server dynamic + send_notification({ + id: "progress-request-#{id}", + method: "window/workDoneProgress/create", + params: { token: id }, + }) + + send_notification({ + method: "$/progress", + params: { + token: id, + value: { + kind: "begin", + title: title, + percentage: percentage, + message: message, + }, + }, + }) + end + + def report_progress(id, percentage: nil, message: nil) + return unless @capabilities[:supports_progress] + + send_notification({ + method: "$/progress", + params: { + token: id, + value: { + kind: "report", + percentage: percentage, + message: message, + }, + }, + }) + end + + def end_progress(id) + return unless @capabilities[:supports_progress] + + send_notification({ + method: "$/progress", + params: { + token: id, + value: { kind: "end" }, + }, + }) + end + + def with_progress(id, title, percentage: nil, message: nil, &block) + progress_block = Progress.new(@stderr, id, @capabilities[:supports_progress]) + return block.call(progress_block) unless @capabilities[:supports_progress] + + begin_progress(id, title, percentage: percentage, message: message) + block.call(progress_block) + end_progress(id) + end + private # Write a response message back to the client @@ -84,17 +173,18 @@ 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, stderr) + def finalize_registrations!(stdout, stderr, capabilities) until @server_addon_classes.empty? - addon = @server_addon_classes.shift.new(stdout, stderr) + addon = @server_addon_classes.shift.new(stdout, stderr, capabilities) @server_addons[addon.name] = addon end end end - def initialize(stdout, stderr) + def initialize(stdout, stderr, capabilities) @stdout = stdout @stderr = stderr + @capabilities = capabilities end def name @@ -181,7 +271,7 @@ def execute(request, params) when "server_addon/register" with_notification_error_handling(request) do require params[:server_addon_path] - ServerAddon.finalize_registrations!(@stdout, @stderr) + ServerAddon.finalize_registrations!(@stdout, @stderr, @capabilities) 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 1eea2997..e10966ca 100644 --- a/test/ruby_lsp_rails/runner_client_test.rb +++ b/test/ruby_lsp_rails/runner_client_test.rb @@ -10,6 +10,13 @@ class RunnerClientTest < ActiveSupport::TestCase setup do @outgoing_queue = Thread::Queue.new @global_state = GlobalState.new + @global_state.apply_options({ + capabilities: { + window: { + workDoneProgress: true, + }, + }, + }) @client = T.let(RunnerClient.new(@outgoing_queue, @global_state), RunnerClient) end @@ -155,6 +162,84 @@ def execute(request, params) ensure FileUtils.rm("server_addon.rb") end + + test "server add-ons can report progress" do + File.write("server_addon.rb", <<~RUBY) + class TapiocaServerAddon < RubyLsp::Rails::ServerAddon + def name + "Tapioca" + end + + def execute(request, params) + begin_progress("my-progress-id", "Doing something expensive") + report_progress("my-progress-id", message: "Made some progress!") + end_progress("my-progress-id") + end + end + RUBY + + @client.register_server_addon(File.expand_path("server_addon.rb")) + @client.delegate_notification(server_addon_name: "Tapioca", request_name: "dsl") + + # Started booting server + pop_log_notification(@outgoing_queue, RubyLsp::Constant::MessageType::LOG) + # Finished booting server + pop_log_notification(@outgoing_queue, RubyLsp::Constant::MessageType::LOG) + + messages = [] + + # Sometimes we get warnings concerning deprecations and they mess up this expectation + until messages.length == 4 + message = @outgoing_queue.pop + messages << message if message.dig(:params, :token) == "my-progress-id" + end + + assert_equal("window/workDoneProgress/create", messages.dig(0, :method)) + assert_equal("begin", messages.dig(1, :params, :value, :kind)) + assert_equal("report", messages.dig(2, :params, :value, :kind)) + assert_equal("end", messages.dig(3, :params, :value, :kind)) + ensure + FileUtils.rm("server_addon.rb") + end + + test "server add-ons can report progress through block API" do + File.write("server_addon.rb", <<~RUBY) + class TapiocaServerAddon < RubyLsp::Rails::ServerAddon + def name + "Tapioca" + end + + def execute(request, params) + with_progress("my-progress-id", "Doing something expensive") do |progress| + progress.report(message: "Made some progress!") + end + end + end + RUBY + + @client.register_server_addon(File.expand_path("server_addon.rb")) + @client.delegate_notification(server_addon_name: "Tapioca", request_name: "dsl") + + # Started booting server + pop_log_notification(@outgoing_queue, RubyLsp::Constant::MessageType::LOG) + # Finished booting server + pop_log_notification(@outgoing_queue, RubyLsp::Constant::MessageType::LOG) + + messages = [] + + # Sometimes we get warnings concerning deprecations and they mess up this expectation + until messages.length == 4 + message = @outgoing_queue.pop + messages << message if message.dig(:params, :token) == "my-progress-id" + end + + assert_equal("window/workDoneProgress/create", messages.dig(0, :method)) + assert_equal("begin", messages.dig(1, :params, :value, :kind)) + assert_equal("report", messages.dig(2, :params, :value, :kind)) + assert_equal("end", messages.dig(3, :params, :value, :kind)) + ensure + FileUtils.rm("server_addon.rb") + end end class NullClientTest < ActiveSupport::TestCase