lib/solargraph/source/cursor.rb



# frozen_string_literal: true


module Solargraph
  class Source
    # Information about a single Position in a Source, including the

    # word located there.

    #

    class Cursor
      # @return [Position]

      attr_reader :position

      # @return [Source]

      attr_reader :source

      # @param source [Source]

      # @param position [Position, Array(Integer, Integer)]

      def initialize source, position
        @source = source
        @position = Position.normalize(position)
      end

      # @return [String]

      def filename
        source.filename
      end

      # The whole word at the current position. Given the text `foo.bar`, the

      # word at position(0,6) is `bar`.

      #

      # @return [String]

      def word
        @word ||= start_of_word + end_of_word
      end

      # The part of the word before the current position. Given the text

      # `foo.bar`, the start_of_word at position(0, 6) is `ba`.

      #

      # @sg-ignore Improve resolution of String#match below

      # @return [String]

      def start_of_word
        @start_of_word ||= begin
          match = source.code[0..offset-1].to_s.match(start_word_pattern)
          result = (match ? match[0] : '')
          # Including the preceding colon if the word appears to be a symbol

          result = ":#{result}" if source.code[0..offset-result.length-1].end_with?(':') and !source.code[0..offset-result.length-1].end_with?('::')
          result
        end
      end

      # The part of the word after the current position. Given the text

      # `foo.bar`, the end_of_word at position (0,6) is `r`.

      #

      # @return [String]

      def end_of_word
        @end_of_word ||= begin
          match = source.code[offset..-1].to_s.match(end_word_pattern)
          match ? match[0] : ''
        end
      end

      # @return [Boolean]

      def start_of_constant?
        source.code[offset-2, 2] == '::'
      end

      # The range of the word at the current position.

      #

      # @return [Range]

      def range
        @range ||= begin
          s = Position.from_offset(source.code, offset - start_of_word.length)
          e = Position.from_offset(source.code, offset + end_of_word.length)
          Solargraph::Range.new(s, e)
        end
      end

      # @return [Chain]

      def chain
        @chain ||= SourceChainer.chain(source, position)
      end

      # True if the statement at the cursor is an argument to a previous

      # method.

      #

      # Given the code `process(foo)`, a cursor pointing at `foo` would

      # identify it as an argument being passed to the `process` method.

      #

      # If #argument? is true, the #recipient method will return a cursor that

      # points to the method receiving the argument.

      #

      # @return [Boolean]

      def argument?
        # @argument ||= !signature_position.nil?

        @argument ||= !recipient.nil?
      end

      # @return [Boolean]

      def comment?
        @comment ||= source.comment_at?(position)
      end

      # @return [Boolean]

      def string?
        @string ||= source.string_at?(position)
      end


      # True if the cursor's chain is an assignment to a variable.

      #

      # When the chain is an assignment, `Cursor#word` will contain the

      # variable name.

      #

      # @return [Boolean]

      def assign?
        [:lvasgn, :ivasgn, :gvasgn, :cvasgn].include? chain&.node&.type
      end

      # Get a cursor pointing to the method that receives the current statement

      # as an argument.

      #

      # @return [Cursor, nil]

      def recipient
        @recipient ||= begin
          node = recipient_node
          node ? Cursor.new(source, Range.from_node(node).ending) : nil
        end
      end
      alias receiver recipient

      # @return [AST::Node]

      def node
        @node ||= source.node_at(position.line, position.column)
      end

      # @return [Position]

      def node_position
        @node_position ||= begin
          if start_of_word.empty?
            match = source.code[0, offset].match(/[\s]*(\.|:+)[\s]*$/)
            if match
              Position.from_offset(source.code, offset - match[0].length)
            else
              position
            end
          else
            position
          end
        end
      end

      # @return [Parser::AST::Node, nil]

      def recipient_node
        @recipient_node ||= Solargraph::Parser::NodeMethods.find_recipient_node(self)
      end

      # @return [Integer]

      def offset
        @offset ||= Position.to_offset(source.code, position)
      end

      private

      # A regular expression to find the start of a word from an offset.

      #

      # @return [Regexp]

      def start_word_pattern
        /(@{1,2}|\$)?([a-z0-9_]|[^\u0000-\u007F])*\z/i
      end

      # A regular expression to find the end of a word from an offset.

      #

      # @return [Regexp]

      def end_word_pattern
        /^([a-z0-9_]|[^\u0000-\u007F])*[\?\!]?/i
      end
    end
  end
end