class Solargraph::LanguageServer::Host


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

def allow_registration method

Returns:
  • (void) -

Parameters:
  • method (String) -- The method name, e.g., 'textDocument/completion'
def allow_registration method
  @dynamic_capabilities.add method
end

def can_register? method

Returns:
  • (Boolean) -

Parameters:
  • method (String) --
def can_register? method
  @dynamic_capabilities.include?(method)
end

def cancel id

Returns:
  • (void) -

Parameters:
  • id (Integer) --
def cancel id
  cancelled.push id
end

def cancel? id

Returns:
  • (Boolean) -

Parameters:
  • id (Integer) --
def cancel? id
  cancelled.include? id
end

def cancelled

Returns:
  • (Array) -
def cancelled
  @cancelled ||= []
end

def catalog

Returns:
  • (void) -
def catalog
  return unless libraries.all?(&:mapped?)
  libraries.each(&:catalog)
end

def change params

Returns:
  • (void) -

Parameters:
  • params (Hash) --
def change params
  updater = generate_updater(params)
  sources.update params['textDocument']['uri'], updater
  diagnoser.schedule params['textDocument']['uri']
end

def check_diff uri, change

Returns:
  • (Hash) -

Parameters:
  • change (Hash) --
  • uri (String) --
def check_diff uri, change
  return change if change['range']
  source = sources.find(uri)
  return change if source.code.length + 1 != change['text'].length
  diffs = Diff::LCS.diff(source.code, change['text'])
  return change if diffs.length.zero? || diffs.length > 1 || diffs.first.length > 1
  # @type [Diff::LCS::Change]

  diff = diffs.first.first
  return change unless diff.adding? && ['.', ':', '(', ',', ' '].include?(diff.element)
  position = Solargraph::Position.from_offset(source.code, diff.position)
  {
    'range' => {
      'start' => {
        'line' => position.line,
        'character' => position.character
      },
      'end' => {
        'line' => position.line,
        'character' => position.character
      }
    },
    'text' => diff.element
  }
rescue Solargraph::FileNotFoundError
  change
end

def clear id

Returns:
  • (void) -

Parameters:
  • id (Integer) --
def clear id
  cancelled.delete id
end

def client_capabilities

Returns:
  • (Hash{String => Hash{String => Boolean}}) -
def client_capabilities
  @client_capabilities ||= {}
end

def client_supports_progress?

def client_supports_progress?
  client_capabilities['window'] && client_capabilities['window']['workDoneProgress']
end

def close uri

Returns:
  • (void) -

Parameters:
  • uri (String) --
def close uri
  logger.info "Closing #{uri}"
  sources.close uri
  diagnoser.schedule uri
end

def completions_at uri, line, column

Returns:
  • (Solargraph::SourceMap::Completion) -

Parameters:
  • column (Integer) --
  • line (Integer) --
  • uri (String) --
def completions_at uri, line, column
  library = library_for(uri)
  library.completions_at uri_to_file(uri), line, column
end

def configure update

Returns:
  • (void) -

Parameters:
  • update (Hash) --
def configure update
  return if update.nil?
  options.merge! update
  logger.level = LOG_LEVELS[options['logLevel']] || DEFAULT_LOG_LEVEL
end

def create *uris

Returns:
  • (Boolean) - True if at least one library accepted at least one file.

Parameters:
  • uris (Array) -- The URIs of the files.
def create *uris
  filenames = uris.map { |uri| uri_to_file(uri) }
  result = false
  libraries.each do |lib|
    result = true if lib.create_from_disk(*filenames)
  end
  uris.each do |uri|
    diagnoser.schedule uri if open?(uri)
  end
  result
end

def default_configuration

Returns:
  • (Hash{String => [Boolean,String]}) -
def default_configuration
  {
    'completion' => true,
    'hover' => true,
    'symbols' => true,
    'definitions' => true,
    'typeDefinitions' => true,
    'rename' => true,
    'references' => true,
    'autoformat' => false,
    'diagnostics' => true,
    'formatting' => false,
    'folding' => true,
    'highlights' => true,
    'logLevel' => 'warn'
  }
end

def definitions_at uri, line, column

Returns:
  • (Array) -

Parameters:
  • column (Integer) --
  • line (Integer) --
  • uri (String) --
def definitions_at uri, line, column
  library = library_for(uri)
  library.definitions_at(uri_to_file(uri), line, column)
end

def delete *uris

Returns:
  • (void) -

Parameters:
  • uris (Array) -- The file uris.
def delete *uris
  filenames = uris.map { |uri| uri_to_file(uri) }
  libraries.each do |lib|
    lib.delete_observer self
    lib.delete(*filenames)
  end
  uris.each do |uri|
    send_notification "textDocument/publishDiagnostics", {
      uri: uri,
      diagnostics: []
    }
  end
end

def diagnose uri

Returns:
  • (void) -

Parameters:
  • uri (String) --
def diagnose uri
  if sources.include?(uri)
    library = library_for(uri)
    if library.mapped? && 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

def diagnoser

Returns:
  • (Diagnoser) -
def diagnoser
  @diagnoser ||= Diagnoser.new(self)
end

def document query

Returns:
  • (Array) -

Parameters:
  • query (String) --
def document query
  result = []
  libraries.each { |lib| result.concat lib.document(query) }
  result
end

def document_symbols uri

Returns:
  • (Array) -

Parameters:
  • uri (String) --
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

def dynamic_capability_options

Returns:
  • (Hash) -
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/typeDefinition' => {
      typeDefinitionProvider: 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
    },
    'textDocument/documentHighlight' => {
      documentHighlightProvider: true
    }
  }
end

def flush

Returns:
  • (String) - The most recent data or an empty string.
def flush
  tmp = ''
  @buffer_semaphore.synchronize do
    tmp = @buffer.clone
    @buffer.clear
  end
  tmp
end

def folders

Returns:
  • (Array) -
def folders
  libraries.map { |lib| lib.workspace.directory }
end

def folding_ranges uri

Returns:
  • (Array) -

Parameters:
  • uri (String) --
def folding_ranges uri
  sources.find(uri).folding_ranges
end

def formatter_config uri

Returns:
  • (Hash) -

Parameters:
  • uri (String) --
def formatter_config uri
  library = library_for(uri)
  library.workspace.config.formatter
end

def generate_updater params

Returns:
  • (Source::Updater) -

Parameters:
  • params (Hash) --
def generate_updater params
  changes = []
  params['contentChanges'].each do |recvd|
    chng = check_diff(params['textDocument']['uri'], recvd)
    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

def has_pending_completions?

Returns:
  • (Bool) - if has pending completion request
def has_pending_completions?
  message_worker.messages.reverse_each.any? { |req| req['method'] == 'textDocument/completion' }
end

def initialize

def initialize
  @buffer_semaphore = Mutex.new
  @request_mutex = Mutex.new
  @buffer = String.new
  @stopped = true
  @next_request_id = 1
  @dynamic_capabilities = Set.new
  @registered_capabilities = Set.new
end

def library_map library

Returns:
  • (void) -

Parameters:
  • library (Library) --
def library_map library
  return if library.mapped?
  Thread.new { sync_library_map library }
end

def locate_pins params

Returns:
  • (Array) -

Parameters:
  • params (Hash) -- A hash representation of a completion item
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'])
    # @todo This exception is necessary because `Library#path_pins` does

    #   not perform a namespace method query, so the implicit `.new` pin

    #   might not exist.

    if result.empty? && params['data']['path'] =~ /\.new$/
      result.concat(library.path_pins(params['data']['path'].sub(/\.new$/, '#initialize')).map do |pin|
        next pin unless pin.name == 'initialize'
        Pin::Method.new(
          name: 'new',
          scope: :class,
          location: pin.location,
          parameters: pin.parameters,
          return_type: ComplexType.try_parse(params['data']['path']),
          comments: pin.comments,
          closure: pin.closure
        )
      end)
    end
  end
  # Selecting by both location and path can result in duplicate pins

  result.uniq { |p| [p.path, p.location] }
end

def message_worker

Returns:
  • (MessageWorker) -
def message_worker
  @message_worker ||= MessageWorker.new(self)
end

def normalize_separators path

Returns:
  • (String) -

Parameters:
  • path (String) --
def normalize_separators path
  return path if File::ALT_SEPARATOR.nil?
  path.gsub(File::ALT_SEPARATOR, File::SEPARATOR)
end

def open uri, text, version

Returns:
  • (void) -

Parameters:
  • version (Integer) -- A version number.
  • text (String) -- The contents of the file.
  • uri (String) -- The file uri.
def open uri, text, version
  src = sources.open(uri, text, version)
  libraries.each do |lib|
    lib.merge src
  end
  diagnoser.schedule uri
end

def open? uri

Returns:
  • (Boolean) -

Parameters:
  • uri (String) --
def open? uri
  sources.include? uri
end

def open_from_disk uri

Returns:
  • (void) -

Parameters:
  • uri (String) --
def open_from_disk uri
  sources.open_from_disk(uri)
  diagnoser.schedule uri
end

def options

Returns:
  • (Hash{String => [Boolean, String]}) -
def options
  @options ||= default_configuration
end

def pending_requests

Returns:
  • (Array) -
def pending_requests
  requests.keys
end

def prepare directory, name = nil

Returns:
  • (void) -

Parameters:
  • name (String, nil) --
  • directory (String) --
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
    workspace = Solargraph::Workspace.new(path, nil, options)
    lib = Solargraph::Library.new(workspace, name)
    lib.add_observer self
    libraries.push lib
    library_map lib
  rescue WorkspaceTooLargeError => e
    send_notification 'window/showMessage', {
      'type' => Solargraph::LanguageServer::MessageTypes::WARNING,
      'message' => e.message
    }
  end
end

def prepare_folders array

Returns:
  • (void) -

Parameters:
  • array (Array String}>) --
def prepare_folders array
  return if array.nil?
  array.each do |folder|
    prepare uri_to_file(folder['uri']), folder['name']
  end
end

def prepare_rename?

def prepare_rename?
  client_capabilities['rename'] && client_capabilities['rename']['prepareSupport']
end

def process request

Returns:
  • (void) -

Parameters:
  • request (Hash) --
def process request
  message_worker.queue(request)
end

def query_symbols query

Returns:
  • (Array) -

Parameters:
  • query (String) --
def query_symbols query
  result = []
  (libraries + [generic_library]).each { |lib| result.concat lib.query_symbols(query) }
  result.uniq
end

def queue message

Returns:
  • (void) -

Parameters:
  • message (String) -- The message to send.
def queue message
  @buffer_semaphore.synchronize { @buffer += message }
  changed
  notify_observers
end

def read_text uri

Returns:
  • (String) -

Parameters:
  • uri (String) --
def read_text uri
  library = library_for(uri)
  filename = uri_to_file(uri)
  library.read_text(filename)
end

def receive request

Returns:
  • (Solargraph::LanguageServer::Message::Base, nil) - The message handler.

Parameters:
  • request (Hash{String => unspecified}) -- The contents of the message.
def receive request
  if request['method']
    logger.info "Host received ##{request['id']} #{request['method']}"
    logger.debug request
    message = Message.select(request['method']).new(self, request)
    begin
      message.process unless cancel?(request['id'])
    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']
    if requests[request['id']]
      requests[request['id']].process(request['result'])
      requests.delete request['id']
    else
      logger.warn "Discarding client response to unrecognized message #{request['id']}"
      nil
    end
  else
    logger.warn "Invalid message received."
    logger.debug request
    nil
  end
end

def references_from uri, line, column, strip: true, only: false

Returns:
  • (Array) -

Parameters:
  • only (Boolean) -- If true, search current file only
  • strip (Boolean) -- Strip special characters from variable names
  • column (Integer) --
  • line (Integer) --
  • uri (String) --
def references_from uri, line, column, strip: true, only: false
  library = library_for(uri)
  library.references_from(uri_to_file(uri), line, column, strip: strip, only: only)
end

def register_capabilities methods

Returns:
  • (void) -

Parameters:
  • methods (Array) -- The methods to register
def register_capabilities methods
  logger.debug "Registering capabilities: #{methods}"
  registrations = methods.select { |m| can_register?(m) and !registered?(m) }.map do |m|
    @registered_capabilities.add m
    {
      id: m,
      method: m,
      registerOptions: dynamic_capability_options[m]
    }
  end
  return if registrations.empty?
  send_request 'client/registerCapability', { registrations: registrations }
end

def registered? method

Returns:
  • (Boolean) -

Parameters:
  • method (String) -- The method name, e.g., 'textDocument/completion'
def registered? method
  @registered_capabilities.include?(method)
end

def remove directory

Returns:
  • (void) -

Parameters:
  • directory (String) --
def remove directory
  logger.info "Removing library for #{directory}"
  # @param lib [Library]

  libraries.delete_if do |lib|
    next false if lib.workspace.directory != directory
    lib.delete_observer self
    true
  end
end

def remove_folders array

Returns:
  • (void) -

Parameters:
  • array (Array) --
def remove_folders array
  array.each do |folder|
    remove uri_to_file(folder['uri'])
  end
end

def requests

Returns:
  • (Hash{Integer => Solargraph::LanguageServer::Host}) -
def requests
  @requests ||= {}
end

def search query

Returns:
  • (Array) -

Parameters:
  • query (String) --
def search query
  result = []
  libraries.each { |lib| result.concat lib.search(query) }
  result
end

def send_notification method, params

Returns:
  • (void) -

Parameters:
  • params (Hash) -- The method parameters
  • method (String) -- The message method
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

def send_request method, params, &block

Returns:
  • (void) -

Other tags:
    Yieldparam: The - result sent by the client

Parameters:
  • block (Proc) -- The block that processes the response
  • params (Hash) -- The method parameters
  • method (String) -- The message method
def send_request method, params, &block
  @request_mutex.synchronize do
    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.debug params
  end
end

def show_message text, type = LanguageServer::MessageTypes::INFO

Returns:
  • (void) -

Parameters:
  • type (Integer) -- A MessageType constant
  • text (String) --
def show_message text, type = LanguageServer::MessageTypes::INFO
  send_notification 'window/showMessage', {
    type: type,
    message: text
  }
end

def show_message_request text, type, actions, &block

Returns:
  • (void) -

Other tags:
    Yieldparam: The - action received from the client

Parameters:
  • block () -- The block that processes the response
  • actions (Array) -- Response options for the client
  • type (Integer) -- A MessageType constant
  • text (String) --
def show_message_request text, type, actions, &block
  send_request 'window/showMessageRequest', {
    type: type,
    message: text,
    actions: actions
  }, &block
end

def signatures_at uri, line, column

Returns:
  • (Array) -

Parameters:
  • column (Integer) --
  • line (Integer) --
  • uri (String) --
def signatures_at uri, line, column
  library = library_for(uri)
  library.signatures_at(uri_to_file(uri), line, column)
end

def start

Returns:
  • (void) -
def start
  return unless stopped?
  @stopped = false
  diagnoser.start
  message_worker.start
end

def stop

Returns:
  • (void) -
def stop
  return if @stopped
  @stopped = true
  message_worker.stop
  diagnoser.stop
  changed
  notify_observers
end

def stopped?

def stopped?
  @stopped
end

def sync_library_map library

Returns:
  • (void) -

Parameters:
  • uuid (String, nil) --
  • library (Library) --
def sync_library_map library
  total = library.workspace.sources.length
  progress = Progress.new('Mapping workspace')
  progress.begin "0/#{total} files", 0
  progress.send self
  while library.next_map
    pct = ((library.source_map_hash.keys.length.to_f / total) * 100).to_i
    progress.report "#{library.source_map_hash.keys.length}/#{total} files", pct
    progress.send self
  end
  progress.finish 'done'
  progress.send self
end

def synchronizing?

def synchronizing?
  !libraries.all?(&:synchronized?)
end

def type_definitions_at uri, line, column

Returns:
  • (Array) -

Parameters:
  • column (Integer) --
  • line (Integer) --
  • uri (String) --
def type_definitions_at uri, line, column
  library = library_for(uri)
  library.type_definitions_at(uri_to_file(uri), line, column)
end

def unregister_capabilities methods

Returns:
  • (void) -

Parameters:
  • methods (Array) -- The methods to unregister
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?
  send_request 'client/unregisterCapability', { unregisterations: unregisterations }
end