Skip to content

Commit 606b1fe

Browse files
authored
Allow server and runtime add-ons to report progress (#561)
Add a bunch of convenience methods to deal with reporting progress for server add-ons. This allows us to display both simple progress or incremental percentage progress.
2 parents 38ddfa8 + 2098600 commit 606b1fe

File tree

2 files changed

+179
-4
lines changed

2 files changed

+179
-4
lines changed

lib/ruby_lsp/ruby_lsp_rails/server.rb

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,33 @@
77
module RubyLsp
88
module Rails
99
module Common
10+
class Progress
11+
def initialize(stderr, id, supports_progress)
12+
@stderr = stderr
13+
@id = id
14+
@supports_progress = supports_progress
15+
end
16+
17+
def report(percentage: nil, message: nil)
18+
return unless @supports_progress
19+
return unless percentage || message
20+
21+
json_message = {
22+
method: "$/progress",
23+
params: {
24+
token: @id,
25+
value: {
26+
kind: "report",
27+
percentage: percentage,
28+
message: message,
29+
},
30+
},
31+
}.to_json
32+
33+
@stderr.write("Content-Length: #{json_message.bytesize}\r\n\r\n#{json_message}")
34+
end
35+
end
36+
1037
# Log a message to the editor's output panel. The type is the number of the message type, which can be found in
1138
# the specification https://microsoft.github.io/language-server-protocol/specification/#messageType
1239
def log_message(message, type: 4)
@@ -50,6 +77,68 @@ def with_notification_error_handling(notification_name, &block)
5077
log_message("Request #{notification_name} failed:\n#{e.full_message(highlight: false)}")
5178
end
5279

80+
def begin_progress(id, title, percentage: nil, message: nil)
81+
return unless @capabilities[:supports_progress]
82+
83+
# This is actually a request, but it is sent asynchronously and we do not return the response back to the
84+
# server, so we consider it a notification from the perspective of the client/runtime server dynamic
85+
send_notification({
86+
id: "progress-request-#{id}",
87+
method: "window/workDoneProgress/create",
88+
params: { token: id },
89+
})
90+
91+
send_notification({
92+
method: "$/progress",
93+
params: {
94+
token: id,
95+
value: {
96+
kind: "begin",
97+
title: title,
98+
percentage: percentage,
99+
message: message,
100+
},
101+
},
102+
})
103+
end
104+
105+
def report_progress(id, percentage: nil, message: nil)
106+
return unless @capabilities[:supports_progress]
107+
108+
send_notification({
109+
method: "$/progress",
110+
params: {
111+
token: id,
112+
value: {
113+
kind: "report",
114+
percentage: percentage,
115+
message: message,
116+
},
117+
},
118+
})
119+
end
120+
121+
def end_progress(id)
122+
return unless @capabilities[:supports_progress]
123+
124+
send_notification({
125+
method: "$/progress",
126+
params: {
127+
token: id,
128+
value: { kind: "end" },
129+
},
130+
})
131+
end
132+
133+
def with_progress(id, title, percentage: nil, message: nil, &block)
134+
progress_block = Progress.new(@stderr, id, @capabilities[:supports_progress])
135+
return block.call(progress_block) unless @capabilities[:supports_progress]
136+
137+
begin_progress(id, title, percentage: percentage, message: message)
138+
block.call(progress_block)
139+
end_progress(id)
140+
end
141+
53142
private
54143

55144
# Write a response message back to the client
@@ -85,17 +174,18 @@ def delegate(name, request, params)
85174
end
86175

87176
# Instantiate all server addons and store them in a hash for easy access after we have discovered the classes
88-
def finalize_registrations!(stdout, stderr)
177+
def finalize_registrations!(stdout, stderr, capabilities)
89178
until @server_addon_classes.empty?
90-
addon = @server_addon_classes.shift.new(stdout, stderr)
179+
addon = @server_addon_classes.shift.new(stdout, stderr, capabilities)
91180
@server_addons[addon.name] = addon
92181
end
93182
end
94183
end
95184

96-
def initialize(stdout, stderr)
185+
def initialize(stdout, stderr, capabilities)
97186
@stdout = stdout
98187
@stderr = stderr
188+
@capabilities = capabilities
99189
end
100190

101191
def name
@@ -182,7 +272,7 @@ def execute(request, params)
182272
when "server_addon/register"
183273
with_notification_error_handling(request) do
184274
require params[:server_addon_path]
185-
ServerAddon.finalize_registrations!(@stdout, @stderr)
275+
ServerAddon.finalize_registrations!(@stdout, @stderr, @capabilities)
186276
end
187277
when "server_addon/delegate"
188278
server_addon_name = params[:server_addon_name]

test/ruby_lsp_rails/runner_client_test.rb

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ class RunnerClientTest < ActiveSupport::TestCase
1010
setup do
1111
@outgoing_queue = Thread::Queue.new
1212
@global_state = GlobalState.new
13+
@global_state.apply_options({
14+
capabilities: {
15+
window: {
16+
workDoneProgress: true,
17+
},
18+
},
19+
})
1320
@client = T.let(RunnerClient.new(@outgoing_queue, @global_state), RunnerClient)
1421
end
1522

@@ -155,6 +162,84 @@ def execute(request, params)
155162
ensure
156163
FileUtils.rm("server_addon.rb")
157164
end
165+
166+
test "server add-ons can report progress" do
167+
File.write("server_addon.rb", <<~RUBY)
168+
class TapiocaServerAddon < RubyLsp::Rails::ServerAddon
169+
def name
170+
"Tapioca"
171+
end
172+
173+
def execute(request, params)
174+
begin_progress("my-progress-id", "Doing something expensive")
175+
report_progress("my-progress-id", message: "Made some progress!")
176+
end_progress("my-progress-id")
177+
end
178+
end
179+
RUBY
180+
181+
@client.register_server_addon(File.expand_path("server_addon.rb"))
182+
@client.delegate_notification(server_addon_name: "Tapioca", request_name: "dsl")
183+
184+
# Started booting server
185+
pop_log_notification(@outgoing_queue, RubyLsp::Constant::MessageType::LOG)
186+
# Finished booting server
187+
pop_log_notification(@outgoing_queue, RubyLsp::Constant::MessageType::LOG)
188+
189+
messages = []
190+
191+
# Sometimes we get warnings concerning deprecations and they mess up this expectation
192+
until messages.length == 4
193+
message = @outgoing_queue.pop
194+
messages << message if message.dig(:params, :token) == "my-progress-id"
195+
end
196+
197+
assert_equal("window/workDoneProgress/create", messages.dig(0, :method))
198+
assert_equal("begin", messages.dig(1, :params, :value, :kind))
199+
assert_equal("report", messages.dig(2, :params, :value, :kind))
200+
assert_equal("end", messages.dig(3, :params, :value, :kind))
201+
ensure
202+
FileUtils.rm("server_addon.rb")
203+
end
204+
205+
test "server add-ons can report progress through block API" do
206+
File.write("server_addon.rb", <<~RUBY)
207+
class TapiocaServerAddon < RubyLsp::Rails::ServerAddon
208+
def name
209+
"Tapioca"
210+
end
211+
212+
def execute(request, params)
213+
with_progress("my-progress-id", "Doing something expensive") do |progress|
214+
progress.report(message: "Made some progress!")
215+
end
216+
end
217+
end
218+
RUBY
219+
220+
@client.register_server_addon(File.expand_path("server_addon.rb"))
221+
@client.delegate_notification(server_addon_name: "Tapioca", request_name: "dsl")
222+
223+
# Started booting server
224+
pop_log_notification(@outgoing_queue, RubyLsp::Constant::MessageType::LOG)
225+
# Finished booting server
226+
pop_log_notification(@outgoing_queue, RubyLsp::Constant::MessageType::LOG)
227+
228+
messages = []
229+
230+
# Sometimes we get warnings concerning deprecations and they mess up this expectation
231+
until messages.length == 4
232+
message = @outgoing_queue.pop
233+
messages << message if message.dig(:params, :token) == "my-progress-id"
234+
end
235+
236+
assert_equal("window/workDoneProgress/create", messages.dig(0, :method))
237+
assert_equal("begin", messages.dig(1, :params, :value, :kind))
238+
assert_equal("report", messages.dig(2, :params, :value, :kind))
239+
assert_equal("end", messages.dig(3, :params, :value, :kind))
240+
ensure
241+
FileUtils.rm("server_addon.rb")
242+
end
158243
end
159244

160245
class NullClientTest < ActiveSupport::TestCase

0 commit comments

Comments
 (0)