Skip to content

Commit

Permalink
Allow server and runtime add-ons to report progress
Browse files Browse the repository at this point in the history
  • Loading branch information
vinistock committed Jan 28, 2025
1 parent fa8c7d6 commit fe4688a
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 4 deletions.
98 changes: 94 additions & 4 deletions lib/ruby_lsp/ruby_lsp_rails/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 } })
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
85 changes: 85 additions & 0 deletions test/ruby_lsp_rails/runner_client_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit fe4688a

Please sign in to comment.