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
Public Class Methods
Source
# File lib/solargraph/language_server/host.rb, line 26 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
Public Instance Methods
Source
# File lib/solargraph/language_server/host.rb, line 433 def allow_registration method @dynamic_capabilities.add method end
Flag a method as available for dynamic registration.
@param method [String] The method name, e.g., âtextDocument/completionâ @return [void]
Source
# File lib/solargraph/language_server/host.rb, line 441 def can_register? method @dynamic_capabilities.include?(method) end
True if the specified LSP method can be dynamically registered.
@param method [String] @return [Boolean]
Source
# File lib/solargraph/language_server/host.rb, line 65 def cancel id cancelled.push id end
Cancel the method with the specified ID.
@param id [Integer] @return [void]
Source
# File lib/solargraph/language_server/host.rb, line 74 def cancel? id cancelled.include? id end
True if the host received a request to cancel the method with the specified ID.
@param id [Integer] @return [Boolean]
Source
# File lib/solargraph/language_server/host.rb, line 692 def catalog return unless libraries.all?(&:mapped?) libraries.each(&:catalog) end
@return [void]
Source
# File lib/solargraph/language_server/host.rb, line 249 def change params updater = generate_updater(params) sources.update params['textDocument']['uri'], updater diagnoser.schedule params['textDocument']['uri'] end
Update a document from the parameters of a textDocument/didChange method.
@param params [Hash] @return [void]
Source
# File lib/solargraph/language_server/host.rb, line 82 def clear id cancelled.delete id end
Delete the specified ID from the list of cancelled IDs if it exists.
@param id [Integer] @return [void]
Source
# File lib/solargraph/language_server/host.rb, line 698 def client_capabilities @client_capabilities ||= {} end
@return [Hash{String => Hash{String => Boolean}}]
Source
# File lib/solargraph/language_server/host.rb, line 702 def client_supports_progress? client_capabilities['window'] && client_capabilities['window']['workDoneProgress'] end
Source
# File lib/solargraph/language_server/host.rb, line 196 def close uri logger.info "Closing #{uri}" sources.close uri diagnoser.schedule uri end
Close the file specified by the URI.
@param uri [String] @return [void]
Source
# File lib/solargraph/language_server/host.rb, line 304 def command_path options['commandPath'] || 'solargraph' end
@return [String]
Source
# File lib/solargraph/language_server/host.rb, line 540 def completions_at uri, line, column library = library_for(uri) library.completions_at uri_to_file(uri), line, column end
@param uri [String] @param line [Integer] @param column [Integer] @return [Solargraph::SourceMap::Completion, nil]
Source
# File lib/solargraph/language_server/host.rb, line 50 def configure update return if update.nil? options.merge! update logger.level = LOG_LEVELS[options['logLevel']] || DEFAULT_LOG_LEVEL end
Update the configuration options with the provided hash.
@param update [Hash] @return [void]
Source
# File lib/solargraph/language_server/host.rb, line 133 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
Respond to a notification that files were created in the workspace. The libraries will determine whether the files should be merged; see Solargraph::Library#create_from_disk.
@param uris [Array<String>] The URIs of the files. @return [Boolean] True if at least one library accepted at least one file.
Source
# File lib/solargraph/language_server/host.rb, line 667 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
@return [Hash{String => [Boolean,String]}]
Source
# File lib/solargraph/language_server/host.rb, line 554 def definitions_at uri, line, column library = library_for(uri) library.definitions_at(uri_to_file(uri), line, column) end
@param uri [String] @param line [Integer] @param column [Integer] @return [Array<Solargraph::Pin::Base>, nil]
Source
# File lib/solargraph/language_server/host.rb, line 149 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
Delete the specified files from the library.
@param uris [Array<String>] The file uris. @return [void]
Source
# File lib/solargraph/language_server/host.rb, line 204 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
@param uri [String] @return [void]
Source
# File lib/solargraph/language_server/host.rb, line 610 def document query result = [] if libraries.empty? result.concat generic_library.document(query) else libraries.each { |lib| result.concat lib.document(query) } end result end
@param query [String] @return [Array]
Source
# File lib/solargraph/language_server/host.rb, line 622 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
@param uri [String] @return [Array<Solargraph::Pin::Base>]
Source
# File lib/solargraph/language_server/host.rb, line 268 def flush tmp = '' @buffer_semaphore.synchronize do tmp = @buffer.clone @buffer.clear end tmp end
Clear the message buffer and return the most recent data.
@return [String] The most recent data or an empty string.
Source
# File lib/solargraph/language_server/host.rb, line 342 def folders libraries.map { |lib| lib.workspace.directory } end
@return [Array<String>]
Source
# File lib/solargraph/language_server/host.rb, line 687 def folding_ranges uri sources.find(uri).folding_ranges end
@param uri [String] @return [Array<Range>]
Source
# File lib/solargraph/language_server/host.rb, line 531 def formatter_config uri library = library_for(uri) library.workspace.config.formatter end
@param uri [String] @return [Hash]
Source
# File lib/solargraph/language_server/host.rb, line 546 def has_pending_completions? message_worker.messages.reverse_each.any? { |req| req['method'] == 'textDocument/completion' } end
@return [Bool] if has pending completion request
Source
# File lib/solargraph/language_server/host.rb, line 477 def locate_pins params return [] unless params['data'] && params['data']['uri'] library = library_for(params['data']['uri']) # @type [Array<Pin::Base>] 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, # @sg-ignore Unresolved call to parameters on Solargraph::Pin::Base parameters: pin.parameters, return_type: ComplexType.try_parse(params['data']['path']), comments: pin.comments, closure: pin.closure, source: :solargraph ) end) end end # Selecting by both location and path can result in duplicate pins result.uniq { |p| [p.path, p.location] } end
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>]
Source
# File lib/solargraph/language_server/host.rb, line 169 def open uri, text, version src = sources.open(uri, text, version) libraries.each do |lib| lib.merge src end diagnoser.schedule uri end
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]
Source
# File lib/solargraph/language_server/host.rb, line 188 def open? uri sources.include? uri end
True if the specified file is currently open in the library.
@param uri [String] @return [Boolean]
Source
# File lib/solargraph/language_server/host.rb, line 179 def open_from_disk uri sources.open_from_disk(uri) diagnoser.schedule uri end
@param uri [String] @return [void]
Source
# File lib/solargraph/language_server/host.rb, line 57 def options @options ||= default_configuration end
@return [Hash{String => [Boolean, String]}]
Source
# File lib/solargraph/language_server/host.rb, line 662 def pending_requests requests.keys end
Get a list of IDs for server requests that are waiting for responses from the client.
@return [Array<Integer>]
Source
# File lib/solargraph/language_server/host.rb, line 282 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
Prepare a library for the specified directory.
@param directory [String] @param name [String, nil] @return [void]
Source
# File lib/solargraph/language_server/host.rb, line 312 def prepare_folders array return if array.nil? array.each do |folder| prepare uri_to_file(folder['uri']), folder['name'] end end
Prepare multiple folders.
@param array [Array<Hash{String => String}>] @return [void]
Source
# File lib/solargraph/language_server/host.rb, line 89 def process request message_worker.queue(request) end
Called by adapter, to handle the request @param request [Hash] @return [void]
Source
# File lib/solargraph/language_server/host.rb, line 594 def query_symbols query result = [] (libraries + [generic_library]).each { |lib| result.concat lib.query_symbols(query) } result.uniq end
@param query [String] @return [Array<Solargraph::Pin::Base>]
Source
# File lib/solargraph/language_server/host.rb, line 259 def queue message @buffer_semaphore.synchronize { @buffer += message } changed notify_observers end
Queue a message to be sent to the client.
@param message [String] The message to send. @return [void]
Source
# File lib/solargraph/language_server/host.rb, line 523 def read_text uri library = library_for(uri) filename = uri_to_file(uri) library.read_text(filename) end
@param uri [String] @return [String]
Source
# File lib/solargraph/language_server/host.rb, line 99 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
Start processing a request from the client. After the message is processed, caller is responsible for sending the response.
@param request [Hash{String => unspecified}] The contents of the message.
@return [Solargraph::LanguageServer::Message::Base, Solargraph::LanguageServer::Request, nil] The message handler.
Source
# File lib/solargraph/language_server/host.rb, line 583 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) rescue FileNotFoundError, InvalidOffsetError => e Solargraph.logger.warn "[#{e.class}] #{e.message}" Solargraph.logger.debug e.backtrace [] end
@param uri [String] @param line [Integer] @param column [Integer] @param strip [Boolean] Strip special characters from variable names @param only [Boolean] If true, search current file only @return [Array<Solargraph::Location>]
Source
# File lib/solargraph/language_server/host.rb, line 396 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
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]
Source
# File lib/solargraph/language_server/host.rb, line 449 def registered? method @registered_capabilities.include?(method) end
True if the specified method has been registered.
@param method [String] The method name, e.g., âtextDocument/completionâ @return [Boolean]
Source
# File lib/solargraph/language_server/host.rb, line 323 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
Remove a directory.
@param directory [String] @return [void]
Source
# File lib/solargraph/language_server/host.rb, line 335 def remove_folders array array.each do |folder| remove uri_to_file(folder['uri']) end end
@param array [Array<Hash>] @return [void]
Source
# File lib/solargraph/language_server/host.rb, line 602 def search query result = [] libraries.each { |lib| result.concat lib.search(query) } result end
@param query [String] @return [Array<String>]
Source
# File lib/solargraph/language_server/host.rb, line 351 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 a notification to the client.
@param method [String] The message method @param params [Hash] The method parameters @return [void]
Source
# File lib/solargraph/language_server/host.rb, line 373 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
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]
Source
# File lib/solargraph/language_server/host.rb, line 635 def show_message text, type = LanguageServer::MessageTypes::INFO send_notification 'window/showMessage', { type: type, message: text } end
Send a notification to the client.
@param text [String] @param type [Integer] A MessageType constant @return [void]
Source
# File lib/solargraph/language_server/host.rb, line 650 def show_message_request text, type, actions, &block send_request 'window/showMessageRequest', { type: type, message: text, actions: actions }, &block end
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]
Source
# File lib/solargraph/language_server/host.rb, line 572 def signatures_at uri, line, column library = library_for(uri) library.signatures_at(uri_to_file(uri), line, column) end
@param uri [String] @param line [Integer] @param column [Integer] @return [Array<Solargraph::Pin::Base>]
Source
# File lib/solargraph/language_server/host.rb, line 39 def start return unless stopped? @stopped = false diagnoser.start message_worker.start end
Start asynchronous process handling.
@return [void]
Source
# File lib/solargraph/language_server/host.rb, line 458 def stop return if @stopped @stopped = true message_worker.stop diagnoser.stop changed notify_observers end
@return [void]
Source
# File lib/solargraph/language_server/host.rb, line 467 def stopped? @stopped end
Source
# File lib/solargraph/language_server/host.rb, line 453 def synchronizing? !libraries.all?(&:synchronized?) end
Source
# File lib/solargraph/language_server/host.rb, line 563 def type_definitions_at uri, line, column library = library_for(uri) library.type_definitions_at(uri_to_file(uri), line, column) end
@param uri [String] @param line [Integer] @param column [Integer] @return [Array<Solargraph::Pin::Base>, nil]
Source
# File lib/solargraph/language_server/host.rb, line 416 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
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]
Private Instance Methods
Source
# File lib/solargraph/language_server/host.rb, line 709 def cancelled @cancelled ||= [] end
@return [Array<Integer>]
Source
# File lib/solargraph/language_server/host.rb, line 762 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 # @sg-ignore push this upstream # @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
@param uri [String] @param change [Hash] @return [Hash]
Source
# File lib/solargraph/language_server/host.rb, line 719 def diagnoser @diagnoser ||= Diagnoser.new(self) end
@return [Diagnoser]
Source
# File lib/solargraph/language_server/host.rb, line 791 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
@return [Hash]
Source
# File lib/solargraph/language_server/host.rb, line 740 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
@param params [Hash] @return [Source::Updater]
Source
# File lib/solargraph/language_server/host.rb, line 855 def library_map library return if library.mapped? Thread.new { sync_library_map library } end
@param library [Library] @return [void]
Source
# File lib/solargraph/language_server/host.rb, line 714 def message_worker @message_worker ||= MessageWorker.new(self) end
@return [MessageWorker]
Source
# File lib/solargraph/language_server/host.rb, line 733 def normalize_separators path return path if File::ALT_SEPARATOR.nil? path.gsub(File::ALT_SEPARATOR, File::SEPARATOR) end
@param path [String] @return [String]
Source
# File lib/solargraph/language_server/host.rb, line 849 def prepare_rename? client_capabilities['rename'] && client_capabilities['rename']['prepareSupport'] end
Source
# File lib/solargraph/language_server/host.rb, line 727 def requests @requests ||= {} end
A hash of client requests by ID. The host uses this to keep track of pending responses.
@return [Hash{Integer => Solargraph::LanguageServer::Request}]
Source
# File lib/solargraph/language_server/host.rb, line 863 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
@param library [Library] @param uuid [String, nil] @return [void]