Skip to content

Commit e3db658

Browse files
committed
Implement workspace/didChangeConfiguration and workspace/configuration
This adds support for the pull model of dynamic configuration changes, as described here: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_configuration Upon receiving `workspace/didChangeConfiguration` message the Ruby LSP server sends `workspace/configuration` to the client. The client replies. This reply is different than other replies handled by Ruby LSP so far, as it does not include `method` and needs to be associated with the request via `id`. This required implementing a collection of server-sent requests, to be able to match. The `result` of the reply is not a hash, instead it's an array of hashes, so it needed to be handled separately as well. Technically, upon receiving `workspace/configuration` the server should check if it should register or unregister some capabilities. I intended to do that too, but it started to become messy and also I did not have a way to properly test it.
1 parent 159e8c9 commit e3db658

File tree

4 files changed

+96
-2
lines changed

4 files changed

+96
-2
lines changed

lib/ruby_lsp/base_server.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def initialize(**options)
1313
@install_error = options[:install_error] #: StandardError?
1414
@incoming_queue = Thread::Queue.new #: Thread::Queue
1515
@outgoing_queue = Thread::Queue.new #: Thread::Queue
16+
@sent_requests = {}
1617
@cancelled_requests = [] #: Array[Integer]
1718
@worker = new_worker #: Thread
1819
@current_request_id = 1 #: Integer
@@ -21,7 +22,15 @@ def initialize(**options)
2122
@outgoing_dispatcher = Thread.new do
2223
unless @test_mode
2324
while (message = @outgoing_queue.pop)
24-
@global_state.synchronize { @writer.write(message.to_hash) }
25+
@global_state.synchronize do
26+
# If the message is a request from server to client, we save it because we might
27+
# need it later to understand the response.
28+
if message.is_a?(Request)
29+
id = message.to_hash[:id]
30+
@sent_requests[id] = message
31+
end
32+
@writer.write(message.to_hash)
33+
end
2534
end
2635
end
2736
end #: Thread

lib/ruby_lsp/server.rb

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ def process_message(message)
8080
type_hierarchy_supertypes(message)
8181
when "typeHierarchy/subtypes"
8282
type_hierarchy_subtypes(message)
83+
when "workspace/didChangeConfiguration"
84+
send_log_message("Reinitializing Ruby LSP after workspace configuration change")
85+
workspace_configuration_did_change(message)
8386
when "workspace/didChangeWatchedFiles"
8487
workspace_did_change_watched_files(message)
8588
when "workspace/symbol"
@@ -157,9 +160,16 @@ def process_message(message)
157160
# Process responses to requests that were sent to the client
158161
#: (Hash[Symbol, untyped] message) -> void
159162
def process_response(message)
160-
case message.dig(:result, :method)
163+
# Some replies have method in their payload, but some do not and we need to match
164+
# the request by id to find what the method is.
165+
method = (message[:result].is_a?(Hash) && message.dig(:result, :method)) || @sent_requests[message[:id]]&.to_hash&.fetch(:method)
166+
167+
case method
161168
when "window/showMessageRequest"
162169
window_show_message_request(message)
170+
when "workspace/configuration"
171+
send_log_message("Received workspace configuration from client: #{message}")
172+
workspace_configuration_received(message)
163173
end
164174
end
165175

@@ -196,6 +206,21 @@ def load_addons(include_project_addons: true)
196206

197207
private
198208

209+
#: (Hash[Symbol, untyped] message) -> void
210+
def workspace_configuration_did_change(message)
211+
# This assumes that the workspace configuration is under "rubyLsp" key, which seems
212+
# to be the standard naming convention.
213+
send_message(Request.workspace_configuration(
214+
@current_request_id, section: "rubyLsp"
215+
))
216+
end
217+
218+
def workspace_configuration_received(message)
219+
options = { initializationOptions: message[:result]&.first }
220+
messages_to_send = @global_state.apply_options(options)
221+
messages_to_send.each { |notification| send_message(notification) }
222+
end
223+
199224
#: (Hash[Symbol, untyped] message) -> void
200225
def run_initialize(message)
201226
options = message[:params]
@@ -273,6 +298,9 @@ def run_initialize(message)
273298
rename_provider: rename_provider,
274299
references_provider: !@global_state.has_type_checker,
275300
document_range_formatting_provider: true,
301+
workspace: {
302+
configuration: true,
303+
},
276304
experimental: {
277305
addon_detection: true,
278306
compose_bundle: true,

lib/ruby_lsp/utils.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,38 @@ def register_watched_files(
175175
),
176176
)
177177
end
178+
179+
def workspace_configuration(
180+
id,
181+
section:
182+
)
183+
new(id: id, method: "workspace/configuration", params: Interface::ConfigurationParams.new(
184+
items: [
185+
Interface::ConfigurationItem.new(section: section),
186+
],
187+
))
188+
end
189+
190+
def client_register_capability(id, type:)
191+
registration = case type
192+
when :formatter
193+
Interface::Registration.new(
194+
id: "ruby-lsp-formatting",
195+
method: "textDocument/formatting",
196+
register_options: {
197+
documentSelector: [
198+
{ language: "ruby" },
199+
{ language: "eruby" },
200+
{ language: "rbs" },
201+
],
202+
},
203+
)
204+
end
205+
206+
new(id: id, method: "client/registerCapability", params: Interface::RegistrationParams.new(
207+
registrations: [registration],
208+
))
209+
end
178210
end
179211

180212
#: (id: (Integer | String), method: String, params: Object) -> void

test/server_test.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,31 @@ def test_backtrace_is_printed_to_stderr_on_exceptions
412412
end
413413
end
414414

415+
def test_reply_to_workspace_configuration_modifies_global_state
416+
@server.instance_variable_set(:@sent_requests, {
417+
1 => RubyLsp::Request.workspace_configuration(
418+
1, section: "rubyLsp"
419+
),
420+
})
421+
422+
@server.process_message({
423+
id: 1,
424+
result: [{ formatter: "standard" }],
425+
})
426+
427+
assert_equal("standard", @server.global_state.formatter)
428+
end
429+
430+
def test_did_change_configuration_sends_workspace_configuration_request
431+
@server.process_message({
432+
id: 1,
433+
method: "workspace/didChangeConfiguration",
434+
params: {},
435+
})
436+
437+
find_message(RubyLsp::Request, "workspace/configuration")
438+
end
439+
415440
def test_changed_file_only_indexes_ruby
416441
path = File.join(Dir.pwd, "lib", "foo.rb")
417442
File.write(path, "class Foo\nend")

0 commit comments

Comments
 (0)