class Solargraph::LanguageServer::Host

The language server protocol's data provider. Hosts are responsible for querying the library and processing messages. They also provide thread safety for multi-threaded transports.

Attributes

client_capabilities[W]

Public Class Methods

new() click to toggle source
# File lib/solargraph/language_server/host.rb, line 25
def initialize
  @cancel_semaphore = Mutex.new
  @buffer_semaphore = Mutex.new
  @register_semaphore = Mutex.new
  @cancel = []
  @buffer = String.new
  @stopped = true
  @next_request_id = 0
  @dynamic_capabilities = Set.new
  @registered_capabilities = Set.new
end

Public Instance Methods

allow_registration(method) click to toggle source

Flag a method as available for dynamic registration.

@param method [String] The method name, e.g., 'textDocument/completion' @return [void]

# File lib/solargraph/language_server/host.rb, line 419
def allow_registration method
  @register_semaphore.synchronize do
    @dynamic_capabilities.add method
  end
end
can_register?(method) click to toggle source

True if the specified LSP method can be dynamically registered.

@param method [String] @return [Boolean]

# File lib/solargraph/language_server/host.rb, line 429
def can_register? method
  @dynamic_capabilities.include?(method)
end
cancel(id) click to toggle source

Cancel the method with the specified ID.

@param id [Integer] @return [void]

# File lib/solargraph/language_server/host.rb, line 67
def cancel id
  @cancel_semaphore.synchronize { @cancel.push id }
end
cancel?(id) click to toggle source

True if the host received a request to cancel the method with the specified ID.

@param id [Integer] @return [Boolean]

# File lib/solargraph/language_server/host.rb, line 76
def cancel? id
  result = false
  @cancel_semaphore.synchronize { result = @cancel.include? id }
  result
end
catalog() click to toggle source

@return [void]

# File lib/solargraph/language_server/host.rb, line 633
def catalog
  libraries.each(&:catalog)
end
change(params) click to toggle source

Update a document from the parameters of a textDocument/didChange method.

@param params [Hash] @return [void]

# File lib/solargraph/language_server/host.rb, line 237
def change params
  updater = generate_updater(params)
  sources.async_update params['textDocument']['uri'], updater
  diagnoser.schedule params['textDocument']['uri']
end
clear(id) click to toggle source

Delete the specified ID from the list of cancelled IDs if it exists.

@param id [Integer] @return [void]

# File lib/solargraph/language_server/host.rb, line 86
def clear id
  @cancel_semaphore.synchronize { @cancel.delete id }
end
client_capabilities() click to toggle source
# File lib/solargraph/language_server/host.rb, line 637
def client_capabilities
  @client_capabilities ||= {}
end
close(uri) click to toggle source

Close the file specified by the URI.

@param uri [String] @return [void]

# File lib/solargraph/language_server/host.rb, line 184
def close uri
  logger.info "Closing #{uri}"
  sources.close uri
  diagnoser.schedule uri
end
completions_at(uri, line, column) click to toggle source

@param uri [String] @param line [Integer] @param column [Integer] @return [Solargraph::SourceMap::Completion]

# File lib/solargraph/language_server/host.rb, line 506
def completions_at uri, line, column
  library = library_for(uri)
  library.completions_at uri_to_file(uri), line, column
end
configure(update) click to toggle source

Update the configuration options with the provided hash.

@param update [Hash] @return [void]

# File lib/solargraph/language_server/host.rb, line 52
def configure update
  return if update.nil?
  options.merge! update
  logger.level = LOG_LEVELS[options['logLevel']] || DEFAULT_LOG_LEVEL
end
create(uri) click to toggle source

Respond to a notification that a file was created in the workspace. The libraries will determine whether the file should be merged; see Solargraph::Library#create_from_disk.

@param uri [String] The file uri. @return [Boolean] True if a library accepted the file.

# File lib/solargraph/language_server/host.rb, line 124
def create uri
  filename = uri_to_file(uri)
  result = false
  libraries.each do |lib|
    result = true if lib.create_from_disk(filename)
  end
  diagnoser.schedule uri if open?(uri)
  result
end
default_configuration() click to toggle source

@return [Hash{String => Object}]

# File lib/solargraph/language_server/host.rb, line 610
def default_configuration
  {
    'completion' => true,
    'hover' => true,
    'symbols' => true,
    'definitions' => true,
    'rename' => true,
    'references' => true,
    'autoformat' => false,
    'diagnostics' => false,
    'formatting' => false,
    'folding' => true,
    'logLevel' => 'warn'
  }
end
definitions_at(uri, line, column) click to toggle source

@param uri [String] @param line [Integer] @param column [Integer] @return [Array<Solargraph::Pin::Base>]

# File lib/solargraph/language_server/host.rb, line 515
def definitions_at uri, line, column
  library = library_for(uri)
  library.definitions_at(uri_to_file(uri), line, column)
end
delete(uri) click to toggle source

Delete the specified file from the library.

@param uri [String] The file uri. @return [void]

# File lib/solargraph/language_server/host.rb, line 138
def delete uri
  filename = uri_to_file(uri)
  libraries.each do |lib|
    lib.delete(filename)
  end
  send_notification "textDocument/publishDiagnostics", {
    uri: uri,
    diagnostics: []
  }
end
diagnose(uri) click to toggle source

@param uri [String] @return [void]

# File lib/solargraph/language_server/host.rb, line 192
def diagnose uri
  if sources.include?(uri)
    library = library_for(uri)
    if library.synchronized?
      logger.info "Diagnosing #{uri}"
      begin
        results = library.diagnose uri_to_file(uri)
        send_notification "textDocument/publishDiagnostics", {
          uri: uri,
          diagnostics: results
        }
      rescue DiagnosticsError => e
        logger.warn "Error in diagnostics: #{e.message}"
        options['diagnostics'] = false
        send_notification 'window/showMessage', {
          type: LanguageServer::MessageTypes::ERROR,
          message: "Error in diagnostics: #{e.message}"
        }
      rescue FileNotFoundError => e
        # @todo This appears to happen when an external file is open and
        #   scheduled for diagnosis, but the file was closed (i.e., the
        #   editor moved to a different file) before diagnosis started
        logger.warn "Unable to diagnose #{uri} : #{e.message}"
        send_notification 'textDocument/publishDiagnostics', {
          uri: uri,
          diagnostics: []
        }
      end
    else
      logger.info "Deferring diagnosis of #{uri}"
      diagnoser.schedule uri
    end
  else
    send_notification 'textDocument/publishDiagnostics', {
      uri: uri,
      diagnostics: []
    }
  end
end
document(query) click to toggle source

@param query [String] @return [Array]

# File lib/solargraph/language_server/host.rb, line 557
def document query
  result = []
  libraries.each { |lib| result.concat lib.document(query) }
  result
end
document_symbols(uri) click to toggle source

@param uri [String] @return [Array<Solargraph::Pin::Base>]

# File lib/solargraph/language_server/host.rb, line 565
def document_symbols uri
  library = library_for(uri)
  # At this level, document symbols should be unique; e.g., a
  # module_function method should return the location for Module.method
  # or Module#method, but not both.
  library.document_symbols(uri_to_file(uri)).uniq(&:location)
end
flush() click to toggle source

Clear the message buffer and return the most recent data.

@return [String] The most recent data or an empty string.

# File lib/solargraph/language_server/host.rb, line 256
def flush
  tmp = ''
  @buffer_semaphore.synchronize do
    tmp = @buffer.clone
    @buffer.clear
  end
  tmp
end
folders() click to toggle source

@return [Array<String>]

# File lib/solargraph/language_server/host.rb, line 321
def folders
  libraries.map { |lib| lib.workspace.directory }
end
folding_ranges(uri) click to toggle source

@param uri [String] @return [Array<Range>]

# File lib/solargraph/language_server/host.rb, line 628
def folding_ranges uri
  sources.find(uri).folding_ranges
end
formatter_config(uri) click to toggle source
# File lib/solargraph/language_server/host.rb, line 497
def formatter_config uri
  library = library_for(uri)
  library.workspace.config.formatter
end
locate_pins(params) click to toggle source

Locate multiple pins that match a completion item. The first match is based on the corresponding location in a library source if available. Subsequent matches are based on path.

@param params [Hash] A hash representation of a completion item @return [Array<Pin::Base>]

# File lib/solargraph/language_server/host.rb, line 466
def locate_pins params
  return [] unless params['data'] && params['data']['uri']
  library = library_for(params['data']['uri'])
  result = []
  if params['data']['location']
    location = Location.new(
      params['data']['location']['filename'],
      Range.from_to(
        params['data']['location']['range']['start']['line'],
        params['data']['location']['range']['start']['character'],
        params['data']['location']['range']['end']['line'],
        params['data']['location']['range']['end']['character']
      )
    )
    result.concat library.locate_pins(location).select{ |pin| pin.name == params['label'] }
  end
  if params['data']['path']
    result.concat library.path_pins(params['data']['path'])
  end
  # Selecting by both location and path can result in duplicate pins
  result.uniq { |p| [p.path, p.location] }
end
open(uri, text, version) click to toggle source

Open the specified file in the library.

@param uri [String] The file uri. @param text [String] The contents of the file. @param version [Integer] A version number. @return [void]

# File lib/solargraph/language_server/host.rb, line 155
def open uri, text, version
  src = sources.open(uri, text, version)
  libraries.each do |lib|
    lib.merge src
  end
  diagnoser.schedule uri
end
open?(uri) click to toggle source

True if the specified file is currently open in the library.

@param uri [String] @return [Boolean]

# File lib/solargraph/language_server/host.rb, line 176
def open? uri
  sources.include? uri
end
open_from_disk(uri) click to toggle source

@param uri [String] @return [void]

# File lib/solargraph/language_server/host.rb, line 165
def open_from_disk uri
  sources.open_from_disk(uri)
  library = library_for(uri)
  # library.open_from_disk uri_to_file(uri)
  diagnoser.schedule uri
end
options() click to toggle source

@return [Hash]

# File lib/solargraph/language_server/host.rb, line 59
def options
  @options ||= default_configuration
end
pending_requests() click to toggle source

Get a list of IDs for server requests that are waiting for responses from the client.

@return [Array<Integer>]

# File lib/solargraph/language_server/host.rb, line 605
def pending_requests
  requests.keys
end
prepare(directory, name = nil) click to toggle source

Prepare a library for the specified directory.

@param directory [String] @param name [String, nil] @return [void]

# File lib/solargraph/language_server/host.rb, line 270
def prepare directory, name = nil
  # No need to create a library without a directory. The generic library
  # will handle it.
  return if directory.nil?
  logger.info "Preparing library for #{directory}"
  path = ''
  path = normalize_separators(directory) unless directory.nil?
  begin
    lib = Solargraph::Library.load(path, name)
    libraries.push lib
  rescue WorkspaceTooLargeError => e
    send_notification 'window/showMessage', {
      'type' => Solargraph::LanguageServer::MessageTypes::WARNING,
      'message' => e.message
    }
  end
end
prepare_folders(array) click to toggle source

Prepare multiple folders.

@param array [Array<Hash{String => String}>] @return [void]

# File lib/solargraph/language_server/host.rb, line 292
def prepare_folders array
  return if array.nil?
  array.each do |folder|
    prepare uri_to_file(folder['uri']), folder['name']
  end
end
query_symbols(query) click to toggle source

@param query [String] @return [Array<Solargraph::Pin::Base>]

# File lib/solargraph/language_server/host.rb, line 541
def query_symbols query
  result = []
  (libraries + [generic_library]).each { |lib| result.concat lib.query_symbols(query) }
  result.uniq
end
queue(message) click to toggle source

Queue a message to be sent to the client.

@param message [String] The message to send. @return [void]

# File lib/solargraph/language_server/host.rb, line 247
def queue message
  @buffer_semaphore.synchronize { @buffer += message }
  changed
  notify_observers
end
read_text(uri) click to toggle source

@param uri [String] @return [String]

# File lib/solargraph/language_server/host.rb, line 491
def read_text uri
  library = library_for(uri)
  filename = uri_to_file(uri)
  library.read_text(filename)
end
receive(request) click to toggle source

Start processing a request from the client. After the message is processed, the transport is responsible for sending the response.

@param request [Hash] The contents of the message. @return [Solargraph::LanguageServer::Message::Base] The message handler.

# File lib/solargraph/language_server/host.rb, line 95
def receive request
  if request['method']
    logger.info "Server received #{request['method']}"
    logger.debug request
    message = Message.select(request['method']).new(self, request)
    begin
      message.process
    rescue StandardError => e
      logger.warn "Error processing request: [#{e.class}] #{e.message}"
      logger.warn e.backtrace.join("\n")
      message.set_error Solargraph::LanguageServer::ErrorCodes::INTERNAL_ERROR, "[#{e.class}] #{e.message}"
    end
    message
  elsif request['id']
    # @todo What if the id is invalid?
    requests[request['id']].process(request['result'])
    requests.delete request['id']
  else
    logger.warn "Invalid message received."
    logger.debug request
  end
end
references_from(uri, line, column, strip: true) click to toggle source

@param uri [String] @param line [Integer] @param column [Integer] @param strip [Boolean] Strip special characters from variable names @return [Array<Solargraph::Range>]

# File lib/solargraph/language_server/host.rb, line 534
def references_from uri, line, column, strip: true
  library = library_for(uri)
  library.references_from(uri_to_file(uri), line, column, strip: strip)
end
register_capabilities(methods) click to toggle source

Register the methods as capabilities with the client. This method will avoid duplicating registrations and ignore methods that were not flagged for dynamic registration by the client.

@param methods [Array<String>] The methods to register @return [void]

# File lib/solargraph/language_server/host.rb, line 374
def register_capabilities methods
  logger.debug "Registering capabilities: #{methods}"
  registrations = methods.select{|m| can_register?(m) and !registered?(m)}.map { |m|
    @registered_capabilities.add m
    {
      id: m,
      method: m,
      registerOptions: dynamic_capability_options[m]
    }
  }
  return if registrations.empty?
  @register_semaphore.synchronize do
    send_request 'client/registerCapability', {
      registrations: registrations
    }
  end
end
registered?(method) click to toggle source

True if the specified method has been registered.

@param method [String] The method name, e.g., 'textDocument/completion' @return [Boolean]

# File lib/solargraph/language_server/host.rb, line 437
def registered? method
  @registered_capabilities.include?(method)
end
remove(directory) click to toggle source

Remove a directory.

@param directory [String] @return [void]

# File lib/solargraph/language_server/host.rb, line 303
def remove directory
  logger.info "Removing library for #{directory}"
  # @param lib [Library]
  libraries.delete_if do |lib|
    next false if lib.workspace.directory != directory
    true
  end
end
remove_folders(array) click to toggle source

@param array [Array<Hash>] @return [void]

# File lib/solargraph/language_server/host.rb, line 314
def remove_folders array
  array.each do |folder|
    remove uri_to_file(folder['uri'])
  end
end
send_notification(method, params) click to toggle source

Send a notification to the client.

@param method [String] The message method @param params [Hash] The method parameters @return [void]

# File lib/solargraph/language_server/host.rb, line 330
def send_notification method, params
  response = {
    jsonrpc: "2.0",
    method: method,
    params: params
  }
  json = response.to_json
  envelope = "Content-Length: #{json.bytesize}\r\n\r\n#{json}"
  queue envelope
  logger.info "Server sent #{method}"
  logger.debug params
end
send_request(method, params, &block) click to toggle source

Send a request to the client and execute the provided block to process the response. If an ID is not provided, the host will use an auto- incrementing integer.

@param method [String] The message method @param params [Hash] The method parameters @param block [Proc] The block that processes the response @yieldparam [Hash] The result sent by the client @return [void]

# File lib/solargraph/language_server/host.rb, line 352
def send_request method, params, &block
  message = {
    jsonrpc: "2.0",
    method: method,
    params: params,
    id: @next_request_id
  }
  json = message.to_json
  requests[@next_request_id] = Request.new(@next_request_id, &block)
  envelope = "Content-Length: #{json.bytesize}\r\n\r\n#{json}"
  queue envelope
  @next_request_id += 1
  logger.info "Server sent #{method}"
  logger.debug params
end
show_message(text, type = LanguageServer::MessageTypes::INFO) click to toggle source

Send a notification to the client.

@param text [String] @param type [Integer] A MessageType constant @return [void]

# File lib/solargraph/language_server/host.rb, line 578
def show_message text, type = LanguageServer::MessageTypes::INFO
  send_notification 'window/showMessage', {
    type: type,
    message: text
  }
end
show_message_request(text, type, actions, &block) click to toggle source

Send a notification with optional responses.

@param text [String] @param type [Integer] A MessageType constant @param actions [Array<String>] Response options for the client @param block The block that processes the response @yieldparam [String] The action received from the client @return [void]

# File lib/solargraph/language_server/host.rb, line 593
def show_message_request text, type, actions, &block
  send_request 'window/showMessageRequest', {
    type: type,
    message: text,
    actions: actions
  }, &block
end
signatures_at(uri, line, column) click to toggle source

@param uri [String] @param line [Integer] @param column [Integer] @return [Array<Solargraph::Pin::Base>]

# File lib/solargraph/language_server/host.rb, line 524
def signatures_at uri, line, column
  library = library_for(uri)
  library.signatures_at(uri_to_file(uri), line, column)
end
start() click to toggle source

Start asynchronous process handling.

@return [void]

# File lib/solargraph/language_server/host.rb, line 40
def start
  return unless stopped?
  @stopped = false
  diagnoser.start
  cataloger.start
  sources.start
end
stop() click to toggle source

@return [void]

# File lib/solargraph/language_server/host.rb, line 446
def stop
  return if @stopped
  @stopped = true
  cataloger.stop
  diagnoser.stop
  sources.stop
  changed
  notify_observers
end
stopped?() click to toggle source
# File lib/solargraph/language_server/host.rb, line 456
def stopped?
  @stopped
end
synchronizing?() click to toggle source
# File lib/solargraph/language_server/host.rb, line 441
def synchronizing?
  !libraries.all?(&:synchronized?)
end
unregister_capabilities(methods) click to toggle source

Unregister the methods with the client. This method will avoid duplicating unregistrations and ignore methods that were not flagged for dynamic registration by the client.

@param methods [Array<String>] The methods to unregister @return [void]

# File lib/solargraph/language_server/host.rb, line 398
def unregister_capabilities methods
  logger.debug "Unregistering capabilities: #{methods}"
  unregisterations = methods.select{|m| registered?(m)}.map{ |m|
    @registered_capabilities.delete m
    {
      id: m,
      method: m
    }
  }
  return if unregisterations.empty?
  @register_semaphore.synchronize do
    send_request 'client/unregisterCapability', {
      unregisterations: unregisterations
    }
  end
end

Private Instance Methods

cataloger() click to toggle source

@return [Cataloger]

# File lib/solargraph/language_server/host.rb, line 649
def cataloger
  @cataloger ||= Cataloger.new(self)
end
diagnoser() click to toggle source

@return [Diagnoser]

# File lib/solargraph/language_server/host.rb, line 644
def diagnoser
  @diagnoser ||= Diagnoser.new(self)
end
dynamic_capability_options() click to toggle source

@return [Hash]

# File lib/solargraph/language_server/host.rb, line 689
def dynamic_capability_options
  @dynamic_capability_options ||= {
    # textDocumentSync: 2, # @todo What should this be?
    'textDocument/completion' => {
      resolveProvider: true,
      triggerCharacters: ['.', ':', '@']
    },
    # hoverProvider: true,
    # definitionProvider: true,
    'textDocument/signatureHelp' => {
      triggerCharacters: ['(', ',', ' ']
    },
    # documentFormattingProvider: true,
    'textDocument/onTypeFormatting' => {
      firstTriggerCharacter: '{',
      moreTriggerCharacter: ['(']
    },
    # documentSymbolProvider: true,
    # workspaceSymbolProvider: true,
    # workspace: {
      # workspaceFolders: {
        # supported: true,
        # changeNotifications: true
      # }
    # }
    'textDocument/definition' => {
      definitionProvider: true
    },
    'textDocument/references' => {
      referencesProvider: true
    },
    'textDocument/rename' => {
      renameProvider: prepare_rename? ? { prepareProvider: true } : true
    },
    'textDocument/documentSymbol' => {
      documentSymbolProvider: true
    },
    'workspace/symbol' => {
      workspaceSymbolProvider: true
    },
    'textDocument/formatting' => {
      formattingProvider: true
    },
    'textDocument/foldingRange' => {
      foldingRangeProvider: true
    },
    'textDocument/codeAction' => {
      codeActionProvider: true
    }
  }
end
generate_updater(params) click to toggle source

@param params [Hash] @return [Source::Updater]

# File lib/solargraph/language_server/host.rb, line 670
def generate_updater params
  changes = []
  params['contentChanges'].each do |chng|
    changes.push Solargraph::Source::Change.new(
      (chng['range'].nil? ? 
        nil :
        Solargraph::Range.from_to(chng['range']['start']['line'], chng['range']['start']['character'], chng['range']['end']['line'], chng['range']['end']['character'])
      ),
      chng['text']
    )
  end
  Solargraph::Source::Updater.new(
    uri_to_file(params['textDocument']['uri']),
    params['textDocument']['version'],
    changes
  )
end
normalize_separators(path) click to toggle source

@param path [String] @return [String]

# File lib/solargraph/language_server/host.rb, line 663
def normalize_separators path
  return path if File::ALT_SEPARATOR.nil?
  path.gsub(File::ALT_SEPARATOR, File::SEPARATOR)
end
prepare_rename?() click to toggle source
# File lib/solargraph/language_server/host.rb, line 741
def prepare_rename?
  client_capabilities['rename'] && client_capabilities['rename']['prepareSupport']
end
requests() click to toggle source

A hash of client requests by ID. The host uses this to keep track of pending responses.

@return [Hash{Integer => Hash}]

# File lib/solargraph/language_server/host.rb, line 657
def requests
  @requests ||= {}
end