diff --git a/lib/puppet-languageserver/language_client.rb b/lib/puppet-languageserver/language_client.rb index 57c87efb..8d8329bb 100644 --- a/lib/puppet-languageserver/language_client.rb +++ b/lib/puppet-languageserver/language_client.rb @@ -2,15 +2,34 @@ module PuppetLanguageServer class LanguageClient - def initialize + attr_reader :message_router + + # Client settings + attr_reader :format_on_type + + def initialize(message_router) + @message_router = message_router @client_capabilites = {} + + # Internal registry of dynamic registrations and their current state + # @registrations[ <[String] method_name>] = [ + # { + # :id => [String] Request ID. Used for de-registration + # :registered => [Boolean] true | false + # :state => [Enum] :pending | :complete + # } + # ] + @registrations = {} + + # Default settings + @format_on_type = false end def client_capability(*names) safe_hash_traverse(@client_capabilites, *names) end - def send_configuration_request(message_router) + def send_configuration_request params = LSP::ConfigurationParams.new.from_h!('items' => []) params.items << LSP::ConfigurationItem.new.from_h!('section' => 'puppet') @@ -22,33 +41,120 @@ def parse_lsp_initialize!(initialize_params = {}) @client_capabilites = initialize_params['capabilities'] end - # Settings could be a hash or an array of hash - def parse_lsp_configuration_settings!(settings = [{}]) - # TODO: Future use. Actually do something with the settings - # settings = [settings] unless settings.is_a?(Hash) - # settings.each do |hash| - # end + def parse_lsp_configuration_settings!(settings = {}) + # format on type + value = safe_hash_traverse(settings, 'puppet', 'editorService', 'formatOnType', 'enable') + unless value.nil? || to_boolean(value) == @format_on_type # rubocop:disable Style/GuardClause Ummm no. + # Is dynamic registration available? + if client_capability('textDocument', 'onTypeFormatting', 'dynamicRegistration') == true + if value + register_capability('textDocument/onTypeFormatting', PuppetLanguageServer::ServerCapabilites.document_on_type_formatting_options) + else + unregister_capability('textDocument/onTypeFormatting') + end + end + @format_on_type = value + end + end + + def capability_registrations(method) + return [{ :registered => false, :state => :complete }] if @registrations[method].nil? || @registrations[method].empty? + @registrations[method].dup end - def register_capability(message_router, method, options = {}) - id = SecureRandom.uuid + def register_capability(method, options = {}) + id = new_request_id PuppetLanguageServer.log_message(:info, "Attempting to dynamically register the #{method} method with id #{id}") + if @registrations[method] && @registrations[method].select { |i| i[:state] == :pending }.count > 0 + # The protocol doesn't specify whether this is allowed and is probably per client specific. For the moment we will allow + # the registration to be sent but log a message that something may be wrong. + PuppetLanguageServer.log_message(:warn, "A dynamic registration/deregistration for the #{method} method is already in progress") + end + params = LSP::RegistrationParams.new.from_h!('registrations' => []) params.registrations << LSP::Registration.new.from_h!('id' => id, 'method' => method, 'registerOptions' => options) + # Note - Don't put more than one method per request even though you can. It makes decoding errors much harder! + + @registrations[method] = [] if @registrations[method].nil? + @registrations[method] << { :registered => false, :state => :pending, :id => id } message_router.json_rpc_handler.send_client_request('client/registerCapability', params) true end - def parse_register_capability_response!(message_router, _response, original_request) + def unregister_capability(method) + if @registrations[method].nil? + PuppetLanguageServer.log_message(:debug, "No registrations to deregister for the #{method}") + return true + end + + params = LSP::UnregistrationParams.new.from_h!('unregisterations' => []) + @registrations[method].each do |reg| + next if reg[:id].nil? + PuppetLanguageServer.log_message(:warn, "A dynamic registration/deregistration for the #{method} method, with id #{reg[:id]} is already in progress") if reg[:state] == :pending + # Ignore registrations that don't need to be unregistered + next if reg[:state] == :complete && !reg[:registered] + params.unregisterations << LSP::Unregistration.new.from_h!('id' => reg[:id], 'method' => method) + reg[:state] = :pending + end + + if params.unregisterations.count.zero? + PuppetLanguageServer.log_message(:debug, "Nothing to deregister for the #{method} method") + return true + end + + message_router.json_rpc_handler.send_client_request('client/unregisterCapability', params) + true + end + + def parse_register_capability_response!(response, original_request) raise 'Response is not from client/registerCapability request' unless original_request['method'] == 'client/registerCapability' + + unless response.key?('result') + original_request['params'].registrations.each do |reg| + # Mark the registration as completed and failed + @registrations[reg.method__lsp] = [] if @registrations[reg.method__lsp].nil? + @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 + end + return true + end + original_request['params'].registrations.each do |reg| PuppetLanguageServer.log_message(:info, "Succesfully dynamically registered the #{reg.method__lsp} method") + + # Mark the registration as completed and succesful + @registrations[reg.method__lsp] = [] if @registrations[reg.method__lsp].nil? + @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 + # If we just registered the workspace/didChangeConfiguration method then # also trigger a configuration request to get the initial state - send_configuration_request(message_router) if reg.method__lsp == 'workspace/didChangeConfiguration' + send_configuration_request if reg.method__lsp == 'workspace/didChangeConfiguration' + end + + true + end + + def parse_unregister_capability_response!(response, original_request) + raise 'Response is not from client/unregisterCapability request' unless original_request['method'] == 'client/unregisterCapability' + + unless response.key?('result') + original_request['params'].unregisterations.each do |reg| + # Mark the registration as completed and failed + @registrations[reg.method__lsp] = [] if @registrations[reg.method__lsp].nil? + @registrations[reg.method__lsp].select { |i| i[:id] == reg.id && i[:registered] }.each { |i| i[:state] = :complete } + @registrations[reg.method__lsp].delete_if { |i| i[:id] == reg.id && !i[:registered] } + end + return true + end + + original_request['params'].unregisterations.each do |reg| + PuppetLanguageServer.log_message(:info, "Succesfully dynamically unregistered the #{reg.method__lsp} method") + + # Remove registrations + @registrations[reg.method__lsp] = [] if @registrations[reg.method__lsp].nil? + @registrations[reg.method__lsp].delete_if { |i| i[:id] == reg.id } end true @@ -56,8 +162,18 @@ def parse_register_capability_response!(message_router, _response, original_requ private + def to_boolean(value) + return false if value.nil? || value == false + return true if value == true + value.to_s =~ %r{^(true|t|yes|y|1)$/i} + end + + def new_request_id + SecureRandom.uuid + end + def safe_hash_traverse(hash, *names) - return nil if names.empty? + return nil if names.empty? || hash.nil? || hash.empty? item = nil loop do name = names.shift diff --git a/lib/puppet-languageserver/message_router.rb b/lib/puppet-languageserver/message_router.rb index d234c556..d72e5dc2 100644 --- a/lib/puppet-languageserver/message_router.rb +++ b/lib/puppet-languageserver/message_router.rb @@ -34,7 +34,7 @@ class MessageRouter < BaseMessageRouter def initialize(options = {}) super @server_options = options.nil? ? {} : options - @client = LanguageClient.new + @client = LanguageClient.new(self) end def documents @@ -46,15 +46,11 @@ def receive_request(request) when 'initialize' PuppetLanguageServer.log_message(:debug, 'Received initialize method') client.parse_lsp_initialize!(request.params) - request.reply_result('capabilities' => PuppetLanguageServer::ServerCapabilites.capabilities) - unless server_options[:puppet_version].nil? || server_options[:puppet_version] == Puppet.version - # Add a minor delay before sending the notification to give the client some processing time - sleep(0.5) - json_rpc_handler.send_show_message_notification( - LSP::MessageType::WARNING, - "Unable to use Puppet version '#{server_options[:puppet_version]}' as it is not available. Using version '#{Puppet.version}' instead." - ) - end + # Setup static registrations if dynamic registration is not available + info = { + :documentOnTypeFormattingProvider => !client.client_capability('textDocument', 'onTypeFormatting', 'dynamicRegistration') + } + request.reply_result('capabilities' => PuppetLanguageServer::ServerCapabilites.capabilities(info)) when 'shutdown' PuppetLanguageServer.log_message(:debug, 'Received shutdown method') @@ -209,6 +205,9 @@ def receive_request(request) request.reply_result(nil) end + when 'textDocument/onTypeFormatting' + request.reply_result(nil) + when 'textDocument/signatureHelp' file_uri = request.params['textDocument']['uri'] line_num = request.params['position']['line'] @@ -253,8 +252,16 @@ def receive_notification(method, params) case method when 'initialized' PuppetLanguageServer.log_message(:info, 'Client has received initialization') + # Raise a warning if the Puppet version is mismatched + unless server_options[:puppet_version].nil? || server_options[:puppet_version] == Puppet.version + json_rpc_handler.send_show_message_notification( + LSP::MessageType::WARNING, + "Unable to use Puppet version '#{server_options[:puppet_version]}' as it is not available. Using version '#{Puppet.version}' instead." + ) + end + # Register for workspace setting changes if it's supported if client.client_capability('workspace', 'didChangeConfiguration', 'dynamicRegistration') == true - client.register_capability(self, 'workspace/didChangeConfiguration') + client.register_capability('workspace/didChangeConfiguration') else PuppetLanguageServer.log_message(:debug, 'Client does not support didChangeConfiguration dynamic registration. Using push method for configuration change detection.') end @@ -300,7 +307,7 @@ def receive_notification(method, params) if params.key?('settings') && params['settings'].nil? # This is a notification from a dynamic registration. Need to send a workspace/configuration # request to get the actual configuration - client.send_configuration_request(self) + client.send_configuration_request else client.parse_lsp_configuration_settings!(params['settings']) end @@ -314,16 +321,21 @@ def receive_notification(method, params) end def receive_response(response, original_request) - unless response.key?('result') + unless receive_response_succesful?(response) # rubocop:disable Style/IfUnlessModifier Line is too long otherwise PuppetLanguageServer.log_message(:error, "Response for method '#{original_request['method']}' with id '#{original_request['id']}' failed with #{response['error']}") - return end - + # Error responses still need to be processed so process messages even if it failed case original_request['method'] when 'client/registerCapability' - client.parse_register_capability_response!(self, response, original_request) + client.parse_register_capability_response!(response, original_request) + when 'client/unregisterCapability' + client.parse_unregister_capability_response!(response, original_request) when 'workspace/configuration' - client.parse_lsp_configuration_settings!(response['result']) + return unless receive_response_succesful?(response) + original_request['params'].items.each_with_index do |item, index| + # The response from the client strips the section name so we need to re-add it + client.parse_lsp_configuration_settings!(item.section => response['result'][index]) + end else super end @@ -331,6 +343,12 @@ def receive_response(response, original_request) PuppetLanguageServer::CrashDump.write_crash_file(e, nil, 'response' => response, 'original_request' => original_request) raise end + + private + + def receive_response_succesful?(response) + response.key?('result') + end end class DisabledMessageRouter < BaseMessageRouter diff --git a/lib/puppet-languageserver/server_capabilities.rb b/lib/puppet-languageserver/server_capabilities.rb index 3ada953d..f42e937e 100644 --- a/lib/puppet-languageserver/server_capabilities.rb +++ b/lib/puppet-languageserver/server_capabilities.rb @@ -2,10 +2,10 @@ module PuppetLanguageServer module ServerCapabilites - def self.capabilities + def self.capabilities(options = {}) # https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#initialize-request - { + value = { 'textDocumentSync' => LSP::TextDocumentSyncKind::FULL, 'hoverProvider' => true, 'completionProvider' => { @@ -19,6 +19,14 @@ def self.capabilities 'triggerCharacters' => ['(', ','] } } + value['documentOnTypeFormattingProvider'] = document_on_type_formatting_options if options[:documentOnTypeFormattingProvider] + value + end + + def self.document_on_type_formatting_options + { + 'firstTriggerCharacter' => '>' + } end def self.no_capabilities diff --git a/spec/languageserver/unit/puppet-languageserver/language_client_spec.rb b/spec/languageserver/unit/puppet-languageserver/language_client_spec.rb index 6ab62fa1..0057a2ba 100644 --- a/spec/languageserver/unit/puppet-languageserver/language_client_spec.rb +++ b/spec/languageserver/unit/puppet-languageserver/language_client_spec.rb @@ -1,8 +1,106 @@ require 'spec_helper' +def pretty_value(value) + value.nil? ? 'nil' : value.to_s +end + +# Requires +# :settings : A hashtable of the inbound settings +# :setting_value : The value that will be set to +RSpec.shared_examples "a client setting" do |method_name| + [ + { :from => false, :setting => nil, :expected_setting => false }, + { :from => false, :setting => false, :expected_setting => false }, + { :from => false, :setting => true, :expected_setting => true }, + { :from => true, :setting => nil, :expected_setting => true }, + { :from => true, :setting => false, :expected_setting => false }, + { :from => true, :setting => true, :expected_setting => true }, + ].each do |testcase| + context "When it transitions from #{pretty_value(testcase[:from])} with a setting value of #{pretty_value(testcase[:setting])}" do + let(:setting_value) { testcase[:setting] } + + before(:each) do + subject.instance_variable_set("@#{method_name}".intern, testcase[:from]) + end + + it "should have a cached value to #{testcase[:expected_setting]}" do + expect(subject.send(method_name)).to eq(testcase[:from]) + + subject.parse_lsp_configuration_settings!(settings) + expect(subject.send(method_name)).to eq(testcase[:expected_setting]) + end + end + end +end + +# Requires +# :settings : A hashtable of the inbound settings +# :setting_value : The value that will be set to +RSpec.shared_examples "a setting with dynamic registrations" do |method_name, dynamic_reg, registration_method| + [ + { :from => false, :setting => nil, :noop => true }, + { :from => false, :setting => false, :noop => true }, + { :from => false, :setting => true, :register => true }, + { :from => true, :setting => nil, :noop => true }, + { :from => true, :setting => false, :unregister => true }, + { :from => true, :setting => true, :noop => true }, + ].each do |testcase| + context "When it transitions from #{pretty_value(testcase[:from])} with a setting value of #{pretty_value(testcase[:setting])}" do + let(:setting_value) { testcase[:setting] } + + before(:each) do + subject.instance_variable_set("@#{method_name}".intern, testcase[:from]) + end + + it 'should not call any capabilities', :if => testcase[:noop] do + expect(subject).to receive(:client_capability).exactly(0).times + expect(subject).to receive(:register_capability).exactly(0).times + expect(subject).to receive(:unregister_capability).exactly(0).times + + subject.parse_lsp_configuration_settings!(settings) + end + + context "when dynamic registration is not supported", :unless => testcase[:noop] do + before(:each) do + expect(subject).to receive(:client_capability).with(*dynamic_reg).and_return(false) + end + + it 'should not call any registration or unregistrations' do + expect(subject).to receive(:register_capability).exactly(0).times + expect(subject).to receive(:unregister_capability).exactly(0).times + + subject.parse_lsp_configuration_settings!(settings) + end + end + + context "when dynamic registration is supported", :unless => testcase[:noop] do + before(:each) do + expect(subject).to receive(:client_capability).with(*dynamic_reg).and_return(true) + end + + it "should register #{registration_method}", :if => testcase[:register] do + expect(subject).to receive(:register_capability).with(registration_method, Object) + expect(subject).to receive(:unregister_capability).exactly(0).times + + subject.parse_lsp_configuration_settings!(settings) + end + + it "should unregister #{registration_method}", :if => testcase[:unregister] do + expect(subject).to receive(:unregister_capability).with(registration_method) + expect(subject).to receive(:register_capability).exactly(0).times + + subject.parse_lsp_configuration_settings!(settings) + end + end + end + end +end + describe 'PuppetLanguageServer::LanguageClient' do + let(:json_rpc_handler) { MockJSONRPCHandler.new } + let(:message_router) { MockMessageRouter.new.tap { |i| i.json_rpc_handler = json_rpc_handler } } let(:subject_options) {} - let(:subject) { PuppetLanguageServer::LanguageClient.new } + let(:subject) { PuppetLanguageServer::LanguageClient.new(message_router) } let(:initialize_params) do # Example capabilities from VS Code { @@ -139,8 +237,16 @@ } } end - let(:json_rpc_handler) { MockJSONRPCHandler.new } - let(:message_router) { MockMessageRouter.new.tap { |i| i.json_rpc_handler = json_rpc_handler } } + + before(:each) do + allow(PuppetLanguageServer).to receive(:log_message) + end + + describe '#format_on_type' do + it 'should be false by default' do + expect(subject.format_on_type).to eq(false) + end + end describe '#client_capability' do before(:each) do @@ -163,11 +269,11 @@ describe '#send_configuration_request' do it 'should send a client request and return true' do expect(json_rpc_handler).to receive(:send_client_request).with('workspace/configuration', Object) - expect(subject.send_configuration_request(message_router)).to eq(true) + expect(subject.send_configuration_request).to eq(true) end it 'should include the puppet settings' do - subject.send_configuration_request(message_router) + subject.send_configuration_request expect(json_rpc_handler.connection.buffer).to include('{"section":"puppet"}') end end @@ -177,7 +283,125 @@ # end describe '#parse_lsp_configuration_settings!' do - # TODO: Future use. + describe 'puppet.editorService.formatOnType.enable' do + let(:settings) do + { 'puppet' => { + 'editorService' => { + 'formatOnType' => { + 'enable' => setting_value + } + } + } + } + end + + it_behaves_like 'a client setting', :format_on_type + + it_behaves_like 'a setting with dynamic registrations', + :format_on_type, + ['textDocument', 'onTypeFormatting', 'dynamicRegistration'], + 'textDocument/onTypeFormatting' + end + end + + describe '#capability_registrations' do + let(:method_name) { 'mockMethod' } + let(:method_options) { {} } + let(:request_id) { 'id001' } + + it 'defaults to false' do + expect(subject.capability_registrations(method_name)).to eq([{:registered => false, :state => :complete}]) + end + + it 'should track the registration process as it completes succesfully' do + req_method_name = nil + req_method_params = nil + # Remember the registration so we can fake a response later + allow(json_rpc_handler).to receive(:send_client_request) do |n, p| + req_method_name = n + req_method_params = p + end + # Fake the request id + allow(subject).to receive(:new_request_id).and_return(request_id) + + # Should start out not registered + expect(subject.capability_registrations(method_name)).to eq([{:registered => false, :state => :complete}]) + # Send as registration request + subject.register_capability(method_name, method_options) + # Should show as in progress + expect(subject.capability_registrations(method_name)).to eq([{:registered => false, :state => :pending, :id => request_id}]) + # Mock a valid response + response = { 'jsonrpc'=>'2.0', 'id'=> 0, 'result' => nil } + original_request = { 'jsonrpc'=>'2.0', 'id' => 0, 'method' => req_method_name, 'params' => req_method_params } + subject.parse_register_capability_response!(response, original_request) + # Should show registered + expect(subject.capability_registrations(method_name)).to eq([{:registered => true, :state => :complete, :id => request_id}]) + end + + it 'should track the registration process as it fails' do + req_method_name = nil + req_method_params = nil + # Remember the registration so we can fake a response later + allow(json_rpc_handler).to receive(:send_client_request) do |n, p| + req_method_name = n + req_method_params = p + end + # Fake the request id + allow(subject).to receive(:new_request_id).and_return(request_id) + + # Should start out not registered + expect(subject.capability_registrations(method_name)).to eq([{:registered => false, :state => :complete}]) + # Send as registration request + subject.register_capability(method_name, method_options) + # Should show as in progress + expect(subject.capability_registrations(method_name)).to eq([{:registered => false, :state => :pending, :id => request_id}]) + # Mock an error response + response = { 'jsonrpc'=>'2.0', 'id'=> 0, 'error' => { 'code' => -1, 'message' => 'mock message' } } + original_request = { 'jsonrpc'=>'2.0', 'id' => 0, 'method' => req_method_name, 'params' => req_method_params } + subject.parse_register_capability_response!(response, original_request) + # Should show registered + expect(subject.capability_registrations(method_name)).to eq([{:registered => false, :state => :complete, :id => request_id}]) + end + + it 'should preserve the registration state until it is completed' do + req_method_name = nil + req_method_params = nil + # Remember the registration so we can fake a response later + allow(json_rpc_handler).to receive(:send_client_request) do |n, p| + req_method_name = n + req_method_params = p + end + # Fake the request id + request_id2 = 'id002' + allow(subject).to receive(:new_request_id).and_return(request_id, request_id2) + + # Should start out not registered + expect(subject.capability_registrations(method_name)).to eq([{:registered => false, :state => :complete}]) + # Send as registration request + subject.register_capability(method_name, method_options) + # Mock a valid response + response = { 'jsonrpc'=>'2.0', 'id'=> 0, 'result' => nil } + original_request = { 'jsonrpc'=>'2.0', 'id' => 0, 'method' => req_method_name, 'params' => req_method_params } + subject.parse_register_capability_response!(response, original_request) + # Should show registered + expect(subject.capability_registrations(method_name)).to eq([{:registered => true, :state => :complete, :id => request_id}]) + # Send another registration request + subject.register_capability(method_name, method_options) + # Should show as in progress + expect(subject.capability_registrations(method_name)).to eq([ + {:registered => true, :state => :complete, :id => request_id}, + {:registered => false, :state => :pending, :id => request_id2} + ]) + # Mock an error response + response = { 'jsonrpc'=>'2.0', 'id'=> 0, 'error' => { 'code' => -1, 'message' => 'mock message' } } + original_request = { 'jsonrpc'=>'2.0', 'id' => 0, 'method' => req_method_name, 'params' => req_method_params } + subject.parse_register_capability_response!(response, original_request) + # Should still show registered + expect(subject.capability_registrations(method_name)).to eq([ + {:registered => true, :state => :complete, :id => request_id}, + {:registered => false, :state => :complete, :id => request_id2} + ]) + end end describe '#register_capability' do @@ -186,18 +410,111 @@ it 'should send a client request and return true' do expect(json_rpc_handler).to receive(:send_client_request).with('client/registerCapability', Object) - expect(subject.register_capability(message_router, method_name, method_options)).to eq(true) + expect(subject.register_capability(method_name, method_options)).to eq(true) end it 'should include the method to register' do - subject.register_capability(message_router, method_name, method_options) + subject.register_capability(method_name, method_options) expect(json_rpc_handler.connection.buffer).to include("\"method\":\"#{method_name}\"") end it 'should include the parameters to register' do - subject.register_capability(message_router, method_name, method_options) + subject.register_capability(method_name, method_options) expect(json_rpc_handler.connection.buffer).to include('"registerOptions":{}') end + + it 'should log a message if a registration is already in progress' do + allow(json_rpc_handler).to receive(:send_client_request) + expect(PuppetLanguageServer).to receive(:log_message).with(:warn, /#{method_name}/) + + subject.register_capability(method_name, method_options) + subject.register_capability(method_name, method_options) + end + + it 'should not log a message if a previous registration completed' do + method_name = nil + method_params = nil + # Remember the registration so we can fake a response later + allow(json_rpc_handler).to receive(:send_client_request) do |n, p| + method_name = n + method_params = p + end + expect(PuppetLanguageServer).to_not receive(:log_message).with(:warn, /#{method_name}/) + # Send as registration request + subject.register_capability(method_name, method_options) + # Mock a valid response + response = { 'jsonrpc'=>'2.0', 'id'=> 0, 'result' => nil } + original_request = { 'jsonrpc'=>'2.0', 'id' => 0, 'method' => method_name, 'params' => method_params } + subject.parse_register_capability_response!(response, original_request) + + subject.register_capability(method_name, method_options) + end + end + + describe '#unregister_capability' do + let(:method_name) { 'mockMethod' } + + before(:each) do + # Mock an already succesful registration + subject.instance_variable_set(:@registrations, { + method_name => [{ :id => 'id001', :state => :complete, :registered => true }] + }) + end + + it 'should send a client request and return true' do + expect(json_rpc_handler).to receive(:send_client_request).with('client/unregisterCapability', Object) + expect(subject.unregister_capability(method_name)).to eq(true) + end + + it 'should include the method to register' do + subject.unregister_capability(method_name) + expect(json_rpc_handler.connection.buffer).to include("\"method\":\"#{method_name}\"") + end + + it 'should log a message if a registration is already in progress' do + allow(json_rpc_handler).to receive(:send_client_request) + expect(PuppetLanguageServer).to receive(:log_message).with(:warn, /#{method_name}/) + + subject.unregister_capability(method_name) + subject.unregister_capability(method_name) + end + + it 'should not log a message if a previous registration completed' do + req_method_name = nil + req_method_params = nil + # Remember the registration so we can fake a response later + allow(json_rpc_handler).to receive(:send_client_request) do |n, p| + req_method_name = n + req_method_params = p + end + + expect(PuppetLanguageServer).to_not receive(:log_message).with(:warn, /#{method_name}/) + # Send as registration request + subject.unregister_capability(method_name) + # Mock a valid response + response = { 'jsonrpc'=>'2.0', 'id'=> 0, 'result' => nil } + original_request = { 'jsonrpc'=>'2.0', 'id' => 0, 'method' => req_method_name, 'params' => req_method_params } + + subject.parse_unregister_capability_response!(response, original_request) + + subject.unregister_capability(method_name) + end + + it 'should not deregister methods that have not been registerd' do + expect(json_rpc_handler).to_not receive(:send_client_request) + + subject.unregister_capability('unknown') + end + + it 'should not deregister methods that are no longer registerd' do + expect(json_rpc_handler).to_not receive(:send_client_request) + + subject.instance_variable_set(:@registrations, { + method_name => [{ :id => 'id001', :state => :complete, :registered => false }] + }) + + subject.unregister_capability(method_name) + end end describe '#parse_register_capability_response!' do @@ -211,7 +528,7 @@ let(:request_params) { {} } it 'should raise an error if the original request was not a registration' do - expect{ subject.parse_register_capability_response!(message_router, response, original_request) }.to raise_error(/client\/registerCapability/) + expect{ subject.parse_register_capability_response!(response, original_request) }.to raise_error(/client\/registerCapability/) end end @@ -224,8 +541,8 @@ end it 'should send a configuration request' do - expect(subject).to receive(:send_configuration_request).with(message_router) - subject.parse_register_capability_response!(message_router, response, original_request) + expect(subject).to receive(:send_configuration_request) + subject.parse_register_capability_response!(response, original_request) end end @@ -237,11 +554,105 @@ params end - it 'should log the registration' do - allow(PuppetLanguageServer).to receive(:log_message) - expect(PuppetLanguageServer).to receive(:log_message).with(:info, /validMethod/) + context 'that failed' do + before(:each) do + response.delete('result') if response.key?('result') + response['error'] = { 'code' => -1, 'message' => 'mock message' } + end + + it 'should not log the registration' do + expect(PuppetLanguageServer).to_not receive(:log_message).with(:info, /validMethod/) + + subject.parse_register_capability_response!(response, original_request) + end + end + + context 'that succeeded' do + it 'should log the registration' do + expect(PuppetLanguageServer).to receive(:log_message).with(:info, /validMethod/) + + subject.parse_register_capability_response!(response, original_request) + end + end + end + end + + describe '#parse_unregister_capability_response!' do + let(:request_id) { 0 } + let(:response_result) { nil } + let(:response) { {'jsonrpc'=>'2.0', 'id'=> request_id, 'result' => response_result } } + let(:original_request) { {'jsonrpc'=>'2.0', 'id'=> request_id, 'method' => request_method, 'params' => request_params} } + let(:method_name) { 'validMethod' } + let(:initial_registration) { true } + + before(:each) do + # Mock an already succesful registration + subject.instance_variable_set(:@registrations, { + method_name => [{ :id => 'id001', :state => :complete, :registered => initial_registration }] + }) + end + + context 'Given an original request that is not an unregistration' do + let(:request_method) { 'mockMethod' } + let(:request_params) { {} } + + it 'should raise an error if the original request was not a registration' do + expect{ subject.parse_unregister_capability_response!(response, original_request) }.to raise_error(/client\/unregisterCapability/) + end + end + + context 'Given a valid original request' do + let(:request_method) { 'client/unregisterCapability' } + let(:request_params) do + params = LSP::UnregistrationParams.new.from_h!('unregisterations' => []) + params.unregisterations << LSP::Unregistration.new.from_h!('id' => 'id001', 'method' => method_name) + params + end + + before(:each) do + # Mimic an unregistration that is in progress + subject.instance_variable_set(:@registrations, { + method_name => [{ :id => 'id001', :state => :pending, :registered => initial_registration }] + }) + end + + context 'that failed' do + before(:each) do + response.delete('result') if response.key?('result') + response['error'] = { 'code' => -1, 'message' => 'mock message' } + end + + context 'and was previously registered' do + it 'should retain that it is registered' do + subject.parse_unregister_capability_response!(response, original_request) + + expect(subject.capability_registrations(method_name)).to eq([{:id=>"id001", :registered=>true, :state=>:complete}]) + end + end + + context 'and was not previously registered' do + let(:initial_registration) { false } + + it 'should no longer be in the registration list' do + subject.parse_unregister_capability_response!(response, original_request) + + expect(subject.capability_registrations(method_name)).to eq([{ :registered => false, :state => :complete }]) + end + end + end + + context 'that succeeded' do + it 'should log the registration' do + expect(PuppetLanguageServer).to receive(:log_message).with(:info, /validMethod/) + + subject.parse_unregister_capability_response!(response, original_request) + end + + it 'should no longer be in the registration list' do + subject.parse_unregister_capability_response!(response, original_request) - subject.parse_register_capability_response!(message_router, response, original_request) + expect(subject.capability_registrations(method_name)).to eq([{ :registered => false, :state => :complete }]) + end end end end diff --git a/spec/languageserver/unit/puppet-languageserver/message_router_spec.rb b/spec/languageserver/unit/puppet-languageserver/message_router_spec.rb index 34bbf79d..090039b1 100644 --- a/spec/languageserver/unit/puppet-languageserver/message_router_spec.rb +++ b/spec/languageserver/unit/puppet-languageserver/message_router_spec.rb @@ -18,6 +18,12 @@ match { |actual| !actual.send(method_name).nil? } end + RSpec::Matchers.define :server_capability do |name| + match do |actual| + actual['capabilities'] && actual['capabilities'][name] + end + end + describe '#documents' do it 'should respond to documents method' do expect(subject).to respond_to(:documents) @@ -67,7 +73,7 @@ # initialize - https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#initialize context 'given an initialize request' do let(:request_rpc_method) { 'initialize' } - let(:request_params) { { 'cap1' => 'value1' } } + let(:request_params) { { 'capabilities' => { 'cap1' => 'value1' } } } it 'should reply with capabilites' do expect(request).to receive(:reply_result).with(hash_including('capabilities')) @@ -79,6 +85,55 @@ subject.receive_request(request) end + + context 'when onTypeFormatting does support dynamic registration' do + let(:request_params) do + { 'capabilities' => { + 'textDocument' => { + 'onTypeFormatting' => { + 'dynamicRegistration' => true + } + } + } + } + end + + it 'should statically register a documentOnTypeFormattingProvider' do + expect(request).to_not receive(:reply_result).with(server_capability('documentOnTypeFormattingProvider')) + allow(request).to receive(:reply_result) + + subject.receive_request(request) + end + end + + context 'when onTypeFormatting does not support dynamic registration' do + let(:request_params) do + { 'capabilities' => { + 'textDocument' => { + 'onTypeFormatting' => { + 'dynamicRegistration' => false + } + } + } + } + end + + it 'should statically register a documentOnTypeFormattingProvider' do + expect(request).to receive(:reply_result).with(server_capability('documentOnTypeFormattingProvider')) + + subject.receive_request(request) + end + end + + context 'when onTypeFormatting does not specify dynamic registration' do + let(:request_params) { {} } + + it 'should statically register a documentOnTypeFormattingProvider' do + expect(request).to receive(:reply_result).with(server_capability('documentOnTypeFormattingProvider')) + + subject.receive_request(request) + end + end end # shutdown - https://github.com/Microsoft/language-server-protocol/blob/master/protocol.md#shutdown @@ -800,6 +855,33 @@ end end + # textDocument/onTypeFormatting - https://microsoft.github.io/language-server-protocol/specification#textDocument_onTypeFormatting + context 'given a textDocument/onTypeFormatting request' do + let(:request_rpc_method) { 'textDocument/onTypeFormatting' } + let(:file_uri) { MANIFEST_FILENAME } + let(:line_num) { 1 } + let(:char_num) { 6 } + let(:trigger_char) { '>' } + let(:formatting_options) { { 'tabSize' => 2, 'insertSpaces' => true} } + let(:request_params) { { + 'textDocument' => { + 'uri' => file_uri + }, + 'position' => { + 'line' => line_num, + 'character' => char_num, + }, + 'ch' => trigger_char, + 'options' => formatting_options + } } + + it 'should not log an error message' do + expect(PuppetLanguageServer).to_not receive(:log_message).with(:error,"Unknown RPC method #{request_rpc_method}") + + subject.receive_request(request) + end + end + context 'given an unknown request' do let(:request_rpc_method) { 'unknown_request_method' } @@ -858,7 +940,7 @@ end it 'should attempt to register workspace/didChangeConfiguration' do - expect(subject.client).to receive(:register_capability).with(Object, 'workspace/didChangeConfiguration') + expect(subject.client).to receive(:register_capability).with('workspace/didChangeConfiguration') subject.receive_notification(notification_method, notification_params) end @@ -1037,7 +1119,7 @@ let(:config_settings) { nil } it 'should send a configuration request' do - expect(subject.client).to receive(:send_configuration_request).with(Object) + expect(subject.client).to receive(:send_configuration_request).with(no_args) subject.receive_notification(notification_method, notification_params) end @@ -1116,19 +1198,23 @@ let(:request_params) { {} } it 'should call client.parse_register_capability_response!' do - expect(subject.client).to receive(:parse_register_capability_response!).with(Object, response, original_request) + expect(subject.client).to receive(:parse_register_capability_response!).with(response, original_request) subject.receive_response(response, original_request) end end context 'given an original workspace/configuration request' do - let(:response_result) { { 'setting1' => 'value1' } } + let(:response_result) { [{ 'setting1' => 'value1' }] } let(:request_method) { 'workspace/configuration'} - let(:request_params) { {} } + let(:request_params) do + params = LSP::ConfigurationParams.new.from_h!('items' => []) + params.items << LSP::ConfigurationItem.new.from_h!('section' => 'mock') + params + end it 'should call client.parse_lsp_configuration_settings!' do - expect(subject.client).to receive(:parse_lsp_configuration_settings!).with(response_result) + expect(subject.client).to receive(:parse_lsp_configuration_settings!).with({ 'mock' => response_result[0] }) subject.receive_response(response, original_request) end