lib/solargraph/source_map.rb



# frozen_string_literal: true


require 'jaro_winkler'
require 'yard'
require 'yard-solargraph'
require 'set'

module Solargraph
  # An index of pins and other ApiMap-related data for a Source.

  #

  class SourceMap
    autoload :Mapper,        'solargraph/source_map/mapper'
    autoload :Clip,          'solargraph/source_map/clip'
    autoload :Completion,    'solargraph/source_map/completion'

    # @return [Source]

    attr_reader :source

    # @return [Array<Pin::Base>]

    attr_reader :pins

    # @return [Array<Pin::Base>]

    attr_reader :locals

    # @param source [Source]

    # @param pins [Array<Pin::Base>]

    # @param locals [Array<Pin::Base>]

    def initialize source, pins, locals
      # HACK: Keep the library from changing this

      @source = source.dup
      @pins = pins
      @locals = locals
      environ.merge Convention.for_local(self) unless filename.nil?
      @pin_class_hash = pins.to_set.classify(&:class).transform_values(&:to_a)
      @pin_select_cache = {}
    end

    def pins_by_class klass
      @pin_select_cache[klass] ||= @pin_class_hash.select { |key, _| key <= klass }.values.flatten
    end

    def rebindable_method_names
      @rebindable_method_names ||= pins_by_class(Pin::Method)
        .select { |pin| pin.comments && pin.comments.include?('@yieldself') }
        .map(&:name)
        .to_set
    end

    # @return [String]

    def filename
      source.filename
    end

    # @return [String]

    def code
      source.code
    end

    # @return [Array<Pin::Reference::Require>]

    def requires
      pins_by_class(Pin::Reference::Require)
    end

    # @return [Environ]

    def environ
      @environ ||= Environ.new
    end

    # @return [Array<Pin::Base>]

    def document_symbols
      @document_symbols ||= pins.select { |pin|
        pin.path && !pin.path.empty?
      }
    end

    # @param query [String]

    # @return [Array<Pin::Base>]

    def query_symbols query
      return document_symbols if query && query.empty?
      document_symbols.select{ |pin| fuzzy_string_match(pin.path, query) || fuzzy_string_match(pin.name, query) }
    end

    # @param position [Position]

    # @return [Source::Cursor]

    def cursor_at position
      Source::Cursor.new(source, position)
    end

    # @param path [String]

    # @return [Pin::Base]

    def first_pin path
      pins.select { |p| p.path == path }.first
    end

    # @param location [Solargraph::Location]

    # @return [Array<Solargraph::Pin::Base>]

    def locate_pins location
      # return nil unless location.start_with?("#{filename}:")

      (pins + locals).select { |pin| pin.location == location }
    end

    def locate_named_path_pin line, character
      _locate_pin line, character, Pin::Namespace, Pin::Method
    end

    def locate_block_pin line, character
      _locate_pin line, character, Pin::Namespace, Pin::Method, Pin::Block
    end

    # @param other_map [SourceMap]

    # @return [Boolean]

    def try_merge! other_map
      return false if pins.length != other_map.pins.length || locals.length != other_map.locals.length || requires.map(&:name).uniq.sort != other_map.requires.map(&:name).uniq.sort
      pins.each_index do |i|
        return false unless pins[i].try_merge!(other_map.pins[i])
      end
      locals.each_index do |i|
        return false unless locals[i].try_merge!(other_map.locals[i])
      end
      @source = other_map.source
      true
    end

    # @param name [String]

    # @return [Array<Location>]

    def references name
      source.references name
    end

    # @param location [Location]

    # @return [Array<Pin::LocalVariable>]

    def locals_at(location)
      return [] if location.filename != filename
      locals.select { |pin| pin.visible_at?(location) }
    end

    class << self
      # @param filename [String]

      # @return [SourceMap]

      def load filename
        source = Solargraph::Source.load(filename)
        SourceMap.map(source)
      end

      # @param code [String]

      # @param filename [String, nil]

      # @return [SourceMap]

      def load_string code, filename = nil
        source = Solargraph::Source.load_string(code, filename)
        SourceMap.map(source)
      end

      # @param source [Source]

      # @return [SourceMap]

      def map source
        result = SourceMap::Mapper.map(source)
        new(source, *result)
      end
    end

    private

    # @param line [Integer]

    # @param character [Integer]

    # @param klasses [Array<Class>]

    # @return [Pin::Base]

    def _locate_pin line, character, *klasses
      position = Position.new(line, character)
      found = nil
      pins.each do |pin|
        # @todo Attribute pins should not be treated like closures, but

        #   there's probably a better way to handle it

        next if pin.is_a?(Pin::Method) && pin.attribute?
        found = pin if (klasses.empty? || klasses.any? { |kls| pin.is_a?(kls) } ) && pin.location.range.contain?(position)
        break if pin.location.range.start.line > line
      end
      # Assuming the root pin is always valid

      found || pins.first
    end

    # @param str1 [String]

    # @param str2 [String]

    # @return [Boolean]

    def fuzzy_string_match str1, str2
      JaroWinkler.distance(str1, str2) > 0.6
    end
  end
end