Skip to content

Commit 58b62de

Browse files
authored
Merge pull request #184 from glennsarti/unreg-client-msgs
(GH-177) Dynamically unregister capabilities
2 parents 95320c2 + b0e3a67 commit 58b62de

File tree

4 files changed

+426
-31
lines changed

4 files changed

+426
-31
lines changed

lib/puppet-languageserver/language_client.rb

Lines changed: 102 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,28 @@
22

33
module PuppetLanguageServer
44
class LanguageClient
5-
def initialize
5+
attr_reader :message_router
6+
7+
def initialize(message_router)
8+
@message_router = message_router
69
@client_capabilites = {}
10+
11+
# Internal registry of dynamic registrations and their current state
12+
# @registrations[ <[String] method_name>] = [
13+
# {
14+
# :id => [String] Request ID. Used for de-registration
15+
# :registered => [Boolean] true | false
16+
# :state => [Enum] :pending | :complete
17+
# }
18+
# ]
19+
@registrations = {}
720
end
821

922
def client_capability(*names)
1023
safe_hash_traverse(@client_capabilites, *names)
1124
end
1225

13-
def send_configuration_request(message_router)
26+
def send_configuration_request
1427
params = LSP::ConfigurationParams.new.from_h!('items' => [])
1528
params.items << LSP::ConfigurationItem.new.from_h!('section' => 'puppet')
1629

@@ -30,32 +43,115 @@ def parse_lsp_configuration_settings!(settings = [{}])
3043
# end
3144
end
3245

33-
def register_capability(message_router, method, options = {})
34-
id = SecureRandom.uuid
46+
def capability_registrations(method)
47+
return [{ :registered => false, :state => :complete }] if @registrations[method].nil? || @registrations[method].empty?
48+
@registrations[method].dup
49+
end
50+
51+
def register_capability(method, options = {})
52+
id = new_request_id
3553

3654
PuppetLanguageServer.log_message(:info, "Attempting to dynamically register the #{method} method with id #{id}")
3755

56+
if @registrations[method] && @registrations[method].select { |i| i[:state] == :pending }.count > 0
57+
# The protocol doesn't specify whether this is allowed and is probably per client specific. For the moment we will allow
58+
# the registration to be sent but log a message that something may be wrong.
59+
PuppetLanguageServer.log_message(:warn, "A dynamic registration/deregistration for the #{method} method is already in progress")
60+
end
61+
3862
params = LSP::RegistrationParams.new.from_h!('registrations' => [])
3963
params.registrations << LSP::Registration.new.from_h!('id' => id, 'method' => method, 'registerOptions' => options)
64+
# Note - Don't put more than one method per request even though you can. It makes decoding errors much harder!
65+
66+
@registrations[method] = [] if @registrations[method].nil?
67+
@registrations[method] << { :registered => false, :state => :pending, :id => id }
4068

4169
message_router.json_rpc_handler.send_client_request('client/registerCapability', params)
4270
true
4371
end
4472

45-
def parse_register_capability_response!(message_router, _response, original_request)
73+
def unregister_capability(method)
74+
if @registrations[method].nil?
75+
PuppetLanguageServer.log_message(:debug, "No registrations to deregister for the #{method}")
76+
return true
77+
end
78+
79+
params = LSP::UnregistrationParams.new.from_h!('unregisterations' => [])
80+
@registrations[method].each do |reg|
81+
next if reg[:id].nil?
82+
PuppetLanguageServer.log_message(:warn, "A dynamic registration/deregistration for the #{method} method, with id #{reg[:id]} is already in progress") if reg[:state] == :pending
83+
# Ignore registrations that don't need to be unregistered
84+
next if reg[:state] == :complete && !reg[:registered]
85+
params.unregisterations << LSP::Unregistration.new.from_h!('id' => reg[:id], 'method' => method)
86+
reg[:state] = :pending
87+
end
88+
89+
if params.unregisterations.count.zero?
90+
PuppetLanguageServer.log_message(:debug, "Nothing to deregister for the #{method} method")
91+
return true
92+
end
93+
94+
message_router.json_rpc_handler.send_client_request('client/unregisterCapability', params)
95+
true
96+
end
97+
98+
def parse_register_capability_response!(response, original_request)
4699
raise 'Response is not from client/registerCapability request' unless original_request['method'] == 'client/registerCapability'
100+
101+
unless response.key?('result')
102+
original_request['params'].registrations.each do |reg|
103+
# Mark the registration as completed and failed
104+
@registrations[reg.method__lsp] = [] if @registrations[reg.method__lsp].nil?
105+
@registrations[reg.method__lsp].select { |i| i[:id] == reg.id }.each { |i| i[:registered] = false; i[:state] = :complete } # rubocop:disable Style/Semicolon This is fine
106+
end
107+
return true
108+
end
109+
47110
original_request['params'].registrations.each do |reg|
48111
PuppetLanguageServer.log_message(:info, "Succesfully dynamically registered the #{reg.method__lsp} method")
112+
113+
# Mark the registration as completed and succesful
114+
@registrations[reg.method__lsp] = [] if @registrations[reg.method__lsp].nil?
115+
@registrations[reg.method__lsp].select { |i| i[:id] == reg.id }.each { |i| i[:registered] = true; i[:state] = :complete } # rubocop:disable Style/Semicolon This is fine
116+
49117
# If we just registered the workspace/didChangeConfiguration method then
50118
# also trigger a configuration request to get the initial state
51-
send_configuration_request(message_router) if reg.method__lsp == 'workspace/didChangeConfiguration'
119+
send_configuration_request if reg.method__lsp == 'workspace/didChangeConfiguration'
120+
end
121+
122+
true
123+
end
124+
125+
def parse_unregister_capability_response!(response, original_request)
126+
raise 'Response is not from client/unregisterCapability request' unless original_request['method'] == 'client/unregisterCapability'
127+
128+
unless response.key?('result')
129+
original_request['params'].unregisterations.each do |reg|
130+
# Mark the registration as completed and failed
131+
@registrations[reg.method__lsp] = [] if @registrations[reg.method__lsp].nil?
132+
@registrations[reg.method__lsp].select { |i| i[:id] == reg.id && i[:registered] }.each { |i| i[:state] = :complete }
133+
@registrations[reg.method__lsp].delete_if { |i| i[:id] == reg.id && !i[:registered] }
134+
end
135+
return true
136+
end
137+
138+
original_request['params'].unregisterations.each do |reg|
139+
PuppetLanguageServer.log_message(:info, "Succesfully dynamically unregistered the #{reg.method__lsp} method")
140+
141+
# Remove registrations
142+
@registrations[reg.method__lsp] = [] if @registrations[reg.method__lsp].nil?
143+
@registrations[reg.method__lsp].delete_if { |i| i[:id] == reg.id }
52144
end
53145

54146
true
55147
end
56148

57149
private
58150

151+
def new_request_id
152+
SecureRandom.uuid
153+
end
154+
59155
def safe_hash_traverse(hash, *names)
60156
return nil if names.empty?
61157
item = nil

lib/puppet-languageserver/message_router.rb

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class MessageRouter < BaseMessageRouter
3434
def initialize(options = {})
3535
super
3636
@server_options = options.nil? ? {} : options
37-
@client = LanguageClient.new
37+
@client = LanguageClient.new(self)
3838
end
3939

4040
def documents
@@ -254,7 +254,7 @@ def receive_notification(method, params)
254254
when 'initialized'
255255
PuppetLanguageServer.log_message(:info, 'Client has received initialization')
256256
if client.client_capability('workspace', 'didChangeConfiguration', 'dynamicRegistration') == true
257-
client.register_capability(self, 'workspace/didChangeConfiguration')
257+
client.register_capability('workspace/didChangeConfiguration')
258258
else
259259
PuppetLanguageServer.log_message(:debug, 'Client does not support didChangeConfiguration dynamic registration. Using push method for configuration change detection.')
260260
end
@@ -300,7 +300,7 @@ def receive_notification(method, params)
300300
if params.key?('settings') && params['settings'].nil?
301301
# This is a notification from a dynamic registration. Need to send a workspace/configuration
302302
# request to get the actual configuration
303-
client.send_configuration_request(self)
303+
client.send_configuration_request
304304
else
305305
client.parse_lsp_configuration_settings!(params['settings'])
306306
end
@@ -314,15 +314,17 @@ def receive_notification(method, params)
314314
end
315315

316316
def receive_response(response, original_request)
317-
unless response.key?('result')
317+
unless receive_response_succesful?(response) # rubocop:disable Style/IfUnlessModifier Line is too long otherwise
318318
PuppetLanguageServer.log_message(:error, "Response for method '#{original_request['method']}' with id '#{original_request['id']}' failed with #{response['error']}")
319-
return
320319
end
321-
320+
# Error responses still need to be processed so process messages even if it failed
322321
case original_request['method']
323322
when 'client/registerCapability'
324-
client.parse_register_capability_response!(self, response, original_request)
323+
client.parse_register_capability_response!(response, original_request)
324+
when 'client/unregisterCapability'
325+
client.parse_unregister_capability_response!(response, original_request)
325326
when 'workspace/configuration'
327+
return unless receive_response_succesful?(response)
326328
client.parse_lsp_configuration_settings!(response['result'])
327329
else
328330
super
@@ -331,6 +333,12 @@ def receive_response(response, original_request)
331333
PuppetLanguageServer::CrashDump.write_crash_file(e, nil, 'response' => response, 'original_request' => original_request)
332334
raise
333335
end
336+
337+
private
338+
339+
def receive_response_succesful?(response)
340+
response.key?('result')
341+
end
334342
end
335343

336344
class DisabledMessageRouter < BaseMessageRouter

0 commit comments

Comments
 (0)