lib/solargraph/source/cursor.rb
# frozen_string_literal: true module Solargraph class Source # Information about a 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`. # # @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 # Get a cursor pointing to the method that receives the current statement # as an argument. # # @return [Cursor, nil] def recipient @recipient ||= begin result = nil node = recipient_node unless node.nil? result = if node.children[1].is_a?(AST::Node) pos = Range.from_node(node.children[1]).start Cursor.new(source, Position.new(pos.line, pos.column - 1)) else Cursor.new(source, Range.from_node(node).ending) end end result end end alias receiver recipient # @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 tree = source.tree_at(position.line, position.column) return tree[1] if tree[1] && tree[1].type == :send && tree[1].children[2..-1].include?(tree[0]) return nil if source.code[offset-1] == ')' || source.code[0..offset] =~ /[^,][ \t]*?\n[ \t]*?\Z/ return nil if first_char_offset < offset && source.code[first_char_offset..offset-1] =~ /\)[\s]*\Z/ pos = Position.from_offset(source.code, first_char_offset) tree = source.tree_at(pos.line, pos.character) if tree[0] && tree[0].type == :send rng = Range.from_node(tree[0]) return tree[0] if (rng.contain?(position) || offset + 1 == Position.to_offset(source.code, rng.ending)) && source.code[offset] =~ /[ \t\)\,'")]/ return tree[0] if (source.code[0..offset-1] =~ /\([\s]*\Z/ || source.code[0..offset-1] =~ /[a-z0-9_][ \t]+\Z/i) end return tree[1] if tree[1] && tree[1].type == :send return tree[3] if tree[1] && tree[3] && tree[1].type == :pair && tree[3].type == :send end private # @return [Integer] def offset @offset ||= Position.to_offset(source.code, position) end # @return [Integer] def first_char_offset @first_char_position ||= begin if source.code[offset - 1] == ')' position else index = offset - 1 index -= 1 while index > 0 && source.code[index].strip.empty? index end end end # 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