Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 129 additions & 13 deletions lib/puppet-languageserver/language_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand All @@ -22,42 +41,139 @@ 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
end

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
Expand Down
52 changes: 35 additions & 17 deletions lib/puppet-languageserver/message_router.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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')
Expand Down Expand Up @@ -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']
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -314,23 +321,34 @@ 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
rescue StandardError => e
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
Expand Down
12 changes: 10 additions & 2 deletions lib/puppet-languageserver/server_capabilities.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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' => {
Expand All @@ -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
Expand Down
Loading