class Solargraph::Library
A Library handles coordination between a Workspace and an ApiMap.
Attributes
@return [LanguageServer::Progress, nil]
@return [Source, nil]
@return [String, nil]
@return [Solargraph::Workspace]
Public Class Methods
Source
# File lib/solargraph/library.rb, line 445 def self.load directory = '', name = nil Solargraph::Library.new(Solargraph::Workspace.new(directory), name) end
Create a library from a directory.
@param directory [String] The path to be used for the workspace @param name [String, nil] @return [Solargraph::Library]
Source
# File lib/solargraph/library.rb, line 28 def initialize workspace = Solargraph::Workspace.new, name = nil @workspace = workspace @name = name # @type [Integer, nil] @total = nil # @type [Source, nil] @current = nil @sync_count = 0 end
@param workspace [Solargraph::Workspace] @param name [String, nil]
Public Instance Methods
Source
# File lib/solargraph/library.rb, line 59 def attach source if @current && (!source || @current.filename != source.filename) && source_map_hash.key?(@current.filename) && !workspace.has_file?(@current.filename) source_map_hash.delete @current.filename source_map_external_require_hash.delete @current.filename @external_requires = nil end changed = source && @current != source @current = source maybe_map @current catalog if changed end
Attach a source to the library.
The attached source does not need to be a part of the workspace. The library will include it in the ApiMap while it’s attached. Only one source can be attached to the library at a time.
@param source [Source, nil] @return [void]
Source
# File lib/solargraph/library.rb, line 75 def attached? filename !@current.nil? && @current.filename == filename end
True if the specified file is currently attached.
@param filename [String] @return [Boolean]
Source
# File lib/solargraph/library.rb, line 431 def bench Bench.new( source_maps: source_map_hash.values, workspace: workspace, external_requires: external_requires, live_map: @current ? source_map_hash[@current.filename] : nil ) end
@return [Bench]
Source
# File lib/solargraph/library.rb, line 426 def catalog @sync_count += 1 end
Update the ApiMap from the library’s workspace and open files.
@return [void]
Source
# File lib/solargraph/library.rb, line 147 def close filename return unless @current&.filename == filename @current = nil catalog unless workspace.has_file?(filename) end
Close a file in the library. Closing a file will make it unavailable for checkout although it may still exist in the workspace.
@param filename [String] @return [void]
Source
# File lib/solargraph/library.rb, line 161 def completions_at filename, line, column sync_catalog position = Position.new(line, column) cursor = Source::Cursor.new(read(filename), position) mutex.synchronize { api_map.clip(cursor).complete } rescue FileNotFoundError => e handle_file_not_found filename, e end
Get completion suggestions at the specified file and location.
@param filename [String] The file to analyze @param line [Integer] The zero-based line number @param column [Integer] The zero-based column number @return [SourceMap::Completion, nil] @todo Take a Location instead of filename/line/column
Source
# File lib/solargraph/library.rb, line 95 def contain? filename workspace.has_file?(filename) end
True if the specified file is included in the workspace (but not necessarily open).
@param filename [String] @return [Boolean]
Source
# File lib/solargraph/library.rb, line 105 def create filename, text return false unless contain?(filename) || open?(filename) source = Solargraph::Source.load_string(text, filename) workspace.merge(source) true end
Create a source to be added to the workspace. The file is ignored if it is neither open in the library nor included in the workspace.
@param filename [String] @param text [String] The contents of the file @return [Boolean] True if the file was added to the workspace.
Source
# File lib/solargraph/library.rb, line 117 def create_from_disk *filenames sources = filenames .reject { |filename| File.directory?(filename) || !File.exist?(filename) } .map { |filename| Solargraph::Source.load_string(File.read(filename), filename) } result = workspace.merge(*sources) sources.each { |source| maybe_map source } result end
Create file sources from files on disk. A file is ignored if it is neither open in the library nor included in the workspace.
@param filenames [Array<String>] @return [Boolean] True if at least one file was added to the workspace.
Source
# File lib/solargraph/library.rb, line 178 def definitions_at filename, line, column sync_catalog position = Position.new(line, column) cursor = Source::Cursor.new(read(filename), position) if cursor.comment? source = read(filename) offset = Solargraph::Position.to_offset(source.code, Solargraph::Position.new(line, column)) lft = source.code[0..offset-1].match(/\[[a-z0-9_:<, ]*?([a-z0-9_:]*)\z/i) rgt = source.code[offset..-1].match(/^([a-z0-9_]*)(:[a-z0-9_:]*)?[\]>, ]/i) if lft && rgt tag = (lft[1] + rgt[1]).sub(/:+$/, '') clip = mutex.synchronize { api_map.clip(cursor) } clip.translate tag else [] end else mutex.synchronize do clip = api_map.clip(cursor) clip.define.map { |pin| pin.realize(api_map) } end end rescue FileNotFoundError => e handle_file_not_found(filename, e) end
Get definition suggestions for the expression at the specified file and location.
@param filename [String] The file to analyze @param line [Integer] The zero-based line number @param column [Integer] The zero-based column number @return [Array<Solargraph::Pin::Base>, nil] @todo Take filename/position instead of filename/line/column
Source
# File lib/solargraph/library.rb, line 132 def delete *filenames result = false filenames.each do |filename| detach filename source_map_hash.delete(filename) result ||= workspace.remove(filename) end result end
Delete files from the library. Deleting a file will make it unavailable for checkout and optionally remove it from the workspace unless the workspace configuration determines that it should still exist.
@param filenames [Array<String>] @return [Boolean] True if any file was deleted
Source
# File lib/solargraph/library.rb, line 84 def detach filename return false if @current.nil? || @current.filename != filename attach nil true end
Detach the specified file if it is currently attached to the library.
@param filename [String] @return [Boolean] True if the specified file was detached
Source
# File lib/solargraph/library.rb, line 391 def diagnose filename # @todo Only open files get diagnosed. Determine whether anything or # everything in the workspace should get diagnosed, or if there should # be an option to do so. # sync_catalog return [] unless open?(filename) result = [] source = read(filename) # @type [Hash{Class<Solargraph::Diagnostics::Base> => Array<String>}] repargs = {} workspace.config.reporters.each do |line| if line == 'all!' Diagnostics.reporters.each do |reporter_name| repargs[Diagnostics.reporter(reporter_name)] ||= [] end else args = line.split(':').map(&:strip) name = args.shift reporter = Diagnostics.reporter(name) raise DiagnosticsError, "Diagnostics reporter #{name} does not exist" if reporter.nil? repargs[reporter] ||= [] repargs[reporter].concat args end end repargs.each_pair do |reporter, args| result.concat reporter.new(*args.uniq).diagnose(source, api_map) end result end
Get diagnostics about a file.
@param filename [String] @return [Array<Hash>]
Source
# File lib/solargraph/library.rb, line 332 def document query sync_catalog mutex.synchronize { [api_map, api_map.get_path_pins(query)] } end
@param query [String] @return [Enumerable<YARD::CodeObjects::Base>] @return [Array(ApiMap, Enumerable<Pin::Base>)]
Source
# File lib/solargraph/library.rb, line 361 def document_symbols filename sync_catalog mutex.synchronize { api_map.document_symbols(filename) } end
Get an array of document symbols.
Document symbols are composed of namespace, method, and constant pins. The results of this query are appropriate for building the response to a textDocument/documentSymbol message in the language server protocol.
@param filename [String] @return [Array<Solargraph::Pin::Base>]
Source
# File lib/solargraph/library.rb, line 497 def external_requires @external_requires ||= source_map_external_require_hash.values.flatten.to_set end
@return [Set<String>]
Source
# File lib/solargraph/library.rb, line 324 def get_path_pins path sync_catalog mutex.synchronize { api_map.get_path_suggestions(path) } end
Get an array of pins that match a path.
@param path [String] @return [Enumerable<Solargraph::Pin::Base>]
Source
# File lib/solargraph/library.rb, line 38 def inspect # Let's not deal with insane data dumps in spec failures to_s end
Source
# File lib/solargraph/library.rb, line 290 def locate_pins location sync_catalog mutex.synchronize { api_map.locate_pins(location).map { |pin| pin.realize(api_map) } } end
Get the pins at the specified location or nil if the pin does not exist.
@param location [Location] @return [Array<Solargraph::Pin::Base>]
Source
# File lib/solargraph/library.rb, line 299 def locate_ref location map = source_map_hash[location.filename] return if map.nil? pin = map.requires.select { |p| p.location.range.contain?(location.range.start) }.first return nil if pin.nil? # @param full [String] return_if_match = proc do |full| if source_map_hash.key?(full) return Location.new(full, Solargraph::Range.from_to(0, 0, 0, 0)) end end workspace.require_paths.each do |path| full = File.join path, pin.name return_if_match.(full) return_if_match.(full << ".rb") end nil rescue FileNotFoundError nil end
Match a require reference to a file.
@param location [Location] @return [Location, nil]
Source
# File lib/solargraph/library.rb, line 483 def map! workspace.sources.each do |src| source_map_hash[src.filename] = Solargraph::SourceMap.map(src) find_external_requires source_map_hash[src.filename] end self end
@return [self]
Source
# File lib/solargraph/library.rb, line 465 def mapped? (workspace.filenames - source_map_hash.keys).empty? end
Source
# File lib/solargraph/library.rb, line 454 def merge source result = workspace.merge(source) maybe_map source result end
Try to merge a source into the library’s workspace. If the workspace is not configured to include the source, it gets ignored.
@param source [Source] @return [Boolean] True if the source was merged into the workspace.
Source
# File lib/solargraph/library.rb, line 470 def next_map return false if mapped? src = workspace.sources.find { |s| !source_map_hash.key?(s.filename) } if src Logging.logger.debug "Mapping #{src.filename}" source_map_hash[src.filename] = Solargraph::SourceMap.map(src) source_map_hash[src.filename] else false end end
@return [SourceMap, Boolean]
Source
# File lib/solargraph/library.rb, line 368 def path_pins path sync_catalog mutex.synchronize { api_map.get_path_suggestions(path) } end
@param path [String] @return [Enumerable<Solargraph::Pin::Base>]
Source
# File lib/solargraph/library.rb, line 492 def pins @pins ||= [] end
@return [Array<Solargraph::Pin::Base>]
Source
# File lib/solargraph/library.rb, line 348 def query_symbols query sync_catalog mutex.synchronize { api_map.query_symbols query } end
Get an array of all symbols in the workspace that match the query.
@param query [String] @return [Array<Pin::Base>]
Source
# File lib/solargraph/library.rb, line 382 def read_text filename source = read(filename) source.code end
Get the current text of a file in the library.
@param filename [String] @return [String]
Source
# File lib/solargraph/library.rb, line 243 def references_from filename, line, column, strip: false, only: false sync_catalog cursor = Source::Cursor.new(read(filename), [line, column]) clip = mutex.synchronize { api_map.clip(cursor) } pin = clip.define.first return [] unless pin result = [] files = if only [api_map.source_map(filename)] else (workspace.sources + (@current ? [@current] : [])) end files.uniq(&:filename).each do |source| found = source.references(pin.name) found.select! do |loc| referenced = definitions_at(loc.filename, loc.range.ending.line, loc.range.ending.character).first referenced&.path == pin.path end if pin.path == 'Class#new' caller = cursor.chain.base.infer(api_map, clip.send(:closure), clip.locals).first if caller.defined? found.select! do |loc| clip = api_map.clip_at(loc.filename, loc.range.start) other = clip.send(:cursor).chain.base.infer(api_map, clip.send(:closure), clip.locals).first caller == other end else found.clear end end # HACK: for language clients that exclude special characters from the start of variable names if strip && match = cursor.word.match(/^[^a-z0-9_]+/i) found.map! do |loc| Solargraph::Location.new(loc.filename, Solargraph::Range.from_to(loc.range.start.line, loc.range.start.column + match[0].length, loc.range.ending.line, loc.range.ending.column)) end end result.concat(found.sort do |a, b| a.range.start.line <=> b.range.start.line end) end result.uniq end
@param filename [String] @param line [Integer] @param column [Integer] @param strip [Boolean] Strip special characters from variable names @param only [Boolean] Search for references in the current file only @return [Array<Solargraph::Location>] @todo Take a Location instead of filename/line/column
Source
# File lib/solargraph/library.rb, line 339 def search query sync_catalog mutex.synchronize { api_map.search query } end
@param query [String] @return [Array<String>]
Source
# File lib/solargraph/library.rb, line 229 def signatures_at filename, line, column sync_catalog position = Position.new(line, column) cursor = Source::Cursor.new(read(filename), position) mutex.synchronize { api_map.clip(cursor).signify } end
Get signature suggestions for the method at the specified file and location.
@param filename [String] The file to analyze @param line [Integer] The zero-based line number @param column [Integer] The zero-based column number @return [Array<Solargraph::Pin::Base>] @todo Take filename/position instead of filename/line/column
Source
# File lib/solargraph/library.rb, line 461 def source_map_hash @source_map_hash ||= {} end
@return [Hash{String => SourceMap}]
Source
# File lib/solargraph/library.rb, line 374 def source_maps source_map_hash.values end
@return [Array<SourceMap>]
Source
# File lib/solargraph/library.rb, line 47 def synchronized? @sync_count < 2 end
True if the ApiMap is up to date with the library’s workspace and open files.
@return [Boolean]
Source
# File lib/solargraph/library.rb, line 212 def type_definitions_at filename, line, column sync_catalog position = Position.new(line, column) cursor = Source::Cursor.new(read(filename), position) mutex.synchronize { api_map.clip(cursor).types } rescue FileNotFoundError => e handle_file_not_found filename, e end
Get type definition suggestions for the expression at the specified file and location.
@param filename [String] The file to analyze @param line [Integer] The zero-based line number @param column [Integer] The zero-based column number @return [Array<Solargraph::Pin::Base>, nil] @todo Take filename/position instead of filename/line/column
Private Instance Methods
Source
# File lib/solargraph/library.rb, line 531 def api_map @api_map ||= Solargraph::ApiMap.new end
@return [ApiMap]
Source
# File lib/solargraph/library.rb, line 575 def cache_errors @cache_errors ||= Set.new end
@return [Set<Gem::Specification>]
Source
# File lib/solargraph/library.rb, line 580 def cache_next_gemspec return if @cache_progress spec = cacheable_specs.first return end_cache_progress unless spec pending = api_map.uncached_gemspecs.length - cache_errors.length - 1 if Yardoc.processing?(spec) logger.info "Enqueuing cache of #{spec.name} #{spec.version} (already being processed)" queued_gemspec_cache.push(spec) return if pending - queued_gemspec_cache.length < 1 catalog sync_catalog else logger.info "Caching #{spec.name} #{spec.version}" Thread.new do report_cache_progress spec.name, pending _o, e, s = Open3.capture3(workspace.command_path, 'cache', spec.name, spec.version.to_s) if s.success? logger.info "Cached #{spec.name} #{spec.version}" else cache_errors.add spec logger.warn "Error caching gemspec #{spec.name} #{spec.version}" logger.warn e end end_cache_progress catalog sync_catalog end end end
@return [void]
Source
# File lib/solargraph/library.rb, line 615 def cacheable_specs cacheable = api_map.uncached_yard_gemspecs + api_map.uncached_rbs_collection_gemspecs - queued_gemspec_cache - cache_errors.to_a return cacheable unless cacheable.empty? queued_gemspec_cache end
@return [Array<Gem::Specification>]
Source
# File lib/solargraph/library.rb, line 660 def end_cache_progress changed if @cache_progress&.finish('done') notify_observers @cache_progress @cache_progress = nil @total = nil end
@return [void]
Source
# File lib/solargraph/library.rb, line 510 def find_external_requires source_map # @type [Set<String>] new_set = source_map.requires.map(&:name).to_set # return if new_set == source_map_external_require_hash[source_map.filename] _filenames = nil filenames = ->{ _filenames ||= workspace.filenames.to_set } source_map_external_require_hash[source_map.filename] = new_set.reject do |path| workspace.require_paths.any? do |base| full = File.join(base, path) filenames[].include?(full) or filenames[].include?(full << ".rb") end end @external_requires = nil end
@param source_map [SourceMap] @return [void]
Source
# File lib/solargraph/library.rb, line 552 def handle_file_not_found filename, error if workspace.source(filename) Solargraph.logger.debug "#{filename} is not cataloged in the ApiMap" nil else raise error end end
@param filename [String] @param error [FileNotFoundError] @return [nil]
Source
# File lib/solargraph/library.rb, line 563 def maybe_map source return unless source return unless @current == source || workspace.has_file?(source.filename) if source_map_hash.key?(source.filename) new_map = Solargraph::SourceMap.map(source) source_map_hash[source.filename] = new_map else source_map_hash[source.filename] = Solargraph::SourceMap.map(source) end end
@param source [Source, nil] @return [void]
Source
# File lib/solargraph/library.rb, line 526 def mutex @mutex ||= Mutex.new end
@return [Thread::Mutex]
Source
# File lib/solargraph/library.rb, line 626 def queued_gemspec_cache @queued_gemspec_cache ||= [] end
@return [Array<Gem::Specification>]
Source
# File lib/solargraph/library.rb, line 543 def read filename return @current if @current && @current.filename == filename raise FileNotFoundError, "File not found: #{filename}" unless workspace.has_file?(filename) workspace.source(filename) end
Get the source for an open file or create a new source if the file exists on disk. Sources created from disk are not added to the open workspace files, i.e., the version on disk remains the authoritative version.
@raise [FileNotFoundError] if the file does not exist @param filename [String] @return [Solargraph::Source]
Source
# File lib/solargraph/library.rb, line 633 def report_cache_progress gem_name, pending @total ||= pending @total = pending if pending > @total finished = @total - pending pct = if @total.zero? 0 else ((finished.to_f / @total.to_f) * 100).to_i end message = "#{gem_name}#{pending > 0 ? " (+#{pending})" : ''}" # " if @cache_progress @cache_progress.report(message, pct) else @cache_progress = LanguageServer::Progress.new('Caching gem') # If we don't send both a begin and a report, the progress notification # might get stuck in the status bar forever @cache_progress.begin(message, pct) changed notify_observers @cache_progress @cache_progress.report(message, pct) end changed notify_observers @cache_progress end
@param gem_name [String] @param pending [Integer] @return [void]
Source
# File lib/solargraph/library.rb, line 504 def source_map_external_require_hash @source_map_external_require_hash ||= {} end
@return [Hash{String => Array<String>}]
Source
# File lib/solargraph/library.rb, line 668 def sync_catalog return if @sync_count == 0 mutex.synchronize do logger.info "Cataloging #{workspace.directory.empty? ? 'generic workspace' : workspace.directory}" source_map_hash.values.each { |map| find_external_requires(map) } api_map.catalog bench logger.info "Catalog complete (#{api_map.source_maps.length} files, #{api_map.pins.length} pins)" logger.info "#{api_map.uncached_yard_gemspecs.length} uncached YARD gemspecs" logger.info "#{api_map.uncached_rbs_collection_gemspecs.length} uncached RBS collection gemspecs" cache_next_gemspec @sync_count = 0 end end
@return [void]