class Solargraph::Source

A Ruby file that has been parsed into an AST.

Constants

FOLDING_NODE_TYPES

Attributes

code[R]

@return [String]

comments[RW]

@return [Hash{Integer => Array<String>}]

error_ranges[W]

@return [Array<Range>]

filename[RW]

@return [String]

last_updater[RW]

@return [Source::Updater]

node[RW]

@return [Parser::AST::Node]

parsed[W]

@return [Boolean]

repaired[RW]

@return [String]

synchronized[W]

@return [Boolean]

version[RW]

@todo Deprecate? @return [Integer]

Public Class Methods

new(code, filename = nil, version = 0) click to toggle source

@param code [String] @param filename [String] @param version [Integer]

# File lib/solargraph/source.rb, line 38
def initialize code, filename = nil, version = 0
  @code = normalize(code)
  @repaired = code
  @filename = filename
  @version = version
  @domains = []
  begin
    # @node, @comments = Source.parse_with_comments(@code, filename)
    @node, @comments = Solargraph::Parser.parse_with_comments(@code, filename)
    @parsed = true
  rescue Parser::SyntaxError, EncodingError => e
    @node = nil
    @comments = {}
    @parsed = false
  ensure
    @code.freeze
  end
end

Private Class Methods

load(filename) click to toggle source

@param filename [String] @return [Solargraph::Source]

# File lib/solargraph/source.rb, line 501
def load filename
  file = File.open(filename)
  code = file.read
  file.close
  Source.load_string(code, filename)
end
load_string(code, filename = nil, version = 0) click to toggle source

@param code [String] @param filename [String] @param version [Integer] @return [Solargraph::Source]

# File lib/solargraph/source.rb, line 512
def load_string code, filename = nil, version = 0
  Source.new code, filename, version
end
parse_docstring(comments) click to toggle source

@param comments [String] @return [YARD::DocstringParser]

# File lib/solargraph/source.rb, line 518
def parse_docstring comments
  # HACK: Pass a dummy code object to the parser for plugins that
  # expect it not to be nil
  YARD::Docstring.parser.parse(comments, YARD::CodeObjects::Base.new(:root, 'stub'))
end

Public Instance Methods

associated_comments() click to toggle source

Get a hash of comments grouped by the line numbers of the associated code.

@return [Hash{Integer => Array<Parser::Source::Comment>}]

# File lib/solargraph/source.rb, line 306
def associated_comments
  @associated_comments ||= begin
    result = {}
    buffer = String.new('')
    last = nil
    @comments.each_pair do |num, snip|
      if !last || num == last + 1
        buffer.concat "#{snip.text}\n"
      else
        result[first_not_empty_from(last + 1)] = buffer.clone
        buffer.replace "#{snip.text}\n"
      end
      last = num
    end
    result[first_not_empty_from(last + 1)] = buffer unless buffer.empty? || last.nil?
    result
  end
end
at(range) click to toggle source

@param range [Solargraph::Range] @return [String]

# File lib/solargraph/source.rb, line 59
def at range
  from_to range.start.line, range.start.character, range.ending.line, range.ending.character
end
code_for(node) click to toggle source

@param node [Parser::AST::Node] @return [String]

# File lib/solargraph/source.rb, line 245
def code_for(node)
  rng = Range.from_node(node)
  b = Position.line_char_to_offset(@code, rng.start.line, rng.start.column)
  e = Position.line_char_to_offset(@code, rng.ending.line, rng.ending.column)
  frag = code[b..e-1].to_s
  frag.strip.gsub(/,$/, '')
end
comment_at?(position) click to toggle source

@param position [Position] @return [Boolean]

# File lib/solargraph/source.rb, line 223
def comment_at? position
  comment_ranges.each do |range|
    return true if range.include?(position) ||
      (range.ending.line == position.line && range.ending.column < position.column)
    break if range.ending.line > position.line
  end
  false
end
comments_for(node) click to toggle source

@param node [Parser::AST::Node] @return [String]

# File lib/solargraph/source.rb, line 255
def comments_for node
  rng = Range.from_node(node)
  stringified_comments[rng.start.line] ||= begin
    buff = associated_comments[rng.start.line]
    buff ? stringify_comment_array(buff) : nil
  end
end
cursor_at(position) click to toggle source

@param position [Position, Array(Integer, Integer)] @return [Source::Cursor]

# File lib/solargraph/source.rb, line 168
def cursor_at position
  Cursor.new(self, position)
end
error_ranges() click to toggle source

@return [Array<Range>]

# File lib/solargraph/source.rb, line 239
def error_ranges
  @error_ranges ||= []
end
finish_synchronize() click to toggle source

Finish synchronizing a source that was updated via start_synchronize. This method returns self if the source is already synchronized. Otherwise it parses the AST and returns a new synchronized Source.

@return [Source]

# File lib/solargraph/source.rb, line 126
def finish_synchronize
  return self if synchronized?
  synced = Source.new(@code, filename)
  if synced.parsed?
    synced.version = version
    return synced
  end
  synced = Source.new(@repaired, filename)
  synced.error_ranges.concat (error_ranges + last_updater.changes.map(&:range))
  synced.code = @code
  synced.synchronized = true
  synced.version = version
  synced
end
folding_ranges() click to toggle source

Get an array of ranges that can be folded, e.g., the range of a class definition or an if condition.

See FOLDING_NODE_TYPES for the list of node types that can be folded.

@return [Array<Range>]

# File lib/solargraph/source.rb, line 289
def folding_ranges
  @folding_ranges ||= begin
    result = []
    inner_folding_ranges node, result
    result.concat foldable_comment_block_ranges
    result
  end
end
from_to(l1, c1, l2, c2) click to toggle source

@param l1 [Integer] @param c1 [Integer] @param l2 [Integer] @param c2 [Integer] @return [String]

# File lib/solargraph/source.rb, line 68
def from_to l1, c1, l2, c2
  b = Solargraph::Position.line_char_to_offset(@code, l1, c1)
  e = Solargraph::Position.line_char_to_offset(@code, l2, c2)
  @code[b..e-1]
end
location() click to toggle source

A location representing the file in its entirety.

@return [Location]

# File lib/solargraph/source.rb, line 266
def location
  st = Position.new(0, 0)
  en = Position.from_offset(code, code.length)
  range = Range.new(st, en)
  Location.new(filename, range)
end
node_at(line, column) click to toggle source

Get the nearest node that contains the specified index.

@param line [Integer] @param column [Integer] @return [AST::Node]

# File lib/solargraph/source.rb, line 79
def node_at(line, column)
  tree_at(line, column).first
end
parsed?() click to toggle source

@return [Boolean]

# File lib/solargraph/source.rb, line 173
def parsed?
  @parsed
end
references(name) click to toggle source

@param name [String] @return [Array<Location>]

# File lib/solargraph/source.rb, line 234
def references name
  Parser.references self, name
end
repaired?() click to toggle source
# File lib/solargraph/source.rb, line 177
def repaired?
  @is_repaired ||= (@code != @repaired)
end
start_synchronize(updater) click to toggle source

Start synchronizing the source. This method updates the code without parsing a new AST. The resulting Source object will be marked not synchronized (synchronized? == false).

@param updater [Source::Updater] @return [Source]

# File lib/solargraph/source.rb, line 103
def start_synchronize updater
  raise 'Invalid synchronization' unless updater.filename == filename
  real_code = updater.write(@code)
  src = Source.allocate
  src.filename = filename
  src.code = real_code
  src.version = updater.version
  src.parsed = parsed?
  src.repaired = updater.repair(@repaired)
  src.synchronized = false
  src.node = @node
  src.comments = @comments
  src.error_ranges = error_ranges
  src.last_updater = updater
  return src.finish_synchronize unless real_code.lines.length == @code.lines.length
  src
end
string_at?(position) click to toggle source

@param position [Position] @return [Boolean]

# File lib/solargraph/source.rb, line 183
def string_at? position
  if Parser.rubyvm?
    string_ranges.each do |range|
      if synchronized?
        return true if range.include?(position) || range.ending == position
      else
        return true if last_updater && last_updater.changes.one? && range.contain?(last_updater.changes.first.range.start)
      end
    end
    false
  else
    return false if Position.to_offset(code, position) >= code.length
    string_nodes.each do |node|
      range = Range.from_node(node)
      next if range.ending.line < position.line
      break if range.ending.line > position.line
      return true if node.type == :str && range.include?(position) && range.start != position
      return true if [:STR, :str].include?(node.type) && range.include?(position) && range.start != position
      if node.type == :dstr
        inner = node_at(position.line, position.column)
        next if inner.nil?
        inner_range = Range.from_node(inner)
        next unless range.include?(inner_range.ending)
        return true if inner.type == :str
        inner_code = at(Solargraph::Range.new(inner_range.start, position))
        return true if (inner.type == :dstr && inner_range.ending.character <= position.character) && !inner_code.end_with?('}') ||
          (inner.type != :dstr && inner_range.ending.line == position.line && position.character <= inner_range.ending.character && inner_code.end_with?('}'))
      end
      break if range.ending.line > position.line
    end
    false
  end
end
string_ranges() click to toggle source
# File lib/solargraph/source.rb, line 217
def string_ranges
  @string_ranges ||= Parser.string_ranges(node)
end
synchronize(updater) click to toggle source

Synchronize the Source with an update. This method applies changes to the code, parses the new code's AST, and returns the resulting Source object.

@param updater [Source::Updater] @return [Source]

# File lib/solargraph/source.rb, line 146
def synchronize updater
  raise 'Invalid synchronization' unless updater.filename == filename
  real_code = updater.write(@code)
  if real_code == @code
    @version = updater.version
    return self
  end
  synced = Source.new(real_code, filename)
  if synced.parsed?
    synced.version = updater.version
    return synced
  end
  incr_code = updater.repair(@repaired)
  synced = Source.new(incr_code, filename)
  synced.error_ranges.concat (error_ranges + updater.changes.map(&:range))
  synced.code = real_code
  synced.version = updater.version
  synced
end
synchronized?() click to toggle source
# File lib/solargraph/source.rb, line 298
def synchronized?
  @synchronized = true if @synchronized.nil?
  @synchronized
end
tree_at(line, column) click to toggle source

Get an array of nodes containing the specified index, starting with the nearest node and ending with the root.

@param line [Integer] @param column [Integer] @return [Array<AST::Node>]

# File lib/solargraph/source.rb, line 89
def tree_at(line, column)
  # offset = Position.line_char_to_offset(@code, line, column)
  position = Position.new(line, column)
  stack = []
  inner_tree_at @node, position, stack
  stack
end

Protected Instance Methods

code=(val) click to toggle source

@param val [String] @return [String]

# File lib/solargraph/source.rb, line 465
def code=(val)
  @code_lines= nil
  @code = val
end

Private Instance Methods

code_lines() click to toggle source

@return [Array<String>]

# File lib/solargraph/source.rb, line 494
def code_lines
  @code_lines ||= code.lines
end
colonized(range, position, node) click to toggle source
# File lib/solargraph/source.rb, line 448
def colonized range, position, node
  node.type == :COLON2 &&
    range.ending.line == position.line &&
    range.ending.character == position.character - 2 &&
    code[Position.to_offset(code, Position.new(position.line, position.character - 2)), 2] == '::'
end
comment_ranges() click to toggle source

@return [Array<Range>]

# File lib/solargraph/source.rb, line 392
def comment_ranges
  @comment_ranges ||= @comments.values.map(&:range)
end
first_not_empty_from(line) click to toggle source
# File lib/solargraph/source.rb, line 327
def first_not_empty_from line
  cursor = line
  cursor += 1 while cursor < code_lines.length && code_lines[cursor].strip.empty?
  cursor = line if cursor > code_lines.length - 1
  cursor
end
foldable_comment_block_ranges() click to toggle source

Get an array of foldable comment block ranges. Blocks are excluded if they are less than 3 lines long.

@return [Array<Range>]

# File lib/solargraph/source.rb, line 400
def foldable_comment_block_ranges
  return [] unless synchronized?
  result = []
  grouped = []
  comments.keys.each do |l|
    if grouped.empty? || l == grouped.last + 1
      grouped.push l
    else
      result.push Range.from_to(grouped.first, 0, grouped.last, 0) unless grouped.length < 3
      grouped = [l]
    end
  end
  result.push Range.from_to(grouped.first, 0, grouped.last, 0) unless grouped.length < 3
  result
end
inner_folding_ranges(top, result = [], parent = nil) click to toggle source

@param top [Parser::AST::Node] @param result [Array<Range>] @param parent [Symbol] @return [void]

# File lib/solargraph/source.rb, line 338
def inner_folding_ranges top, result = [], parent = nil
  # return unless top.is_a?(::Parser::AST::Node)
  return unless Parser.is_ast_node?(top)
  if FOLDING_NODE_TYPES.include?(top.type)
    # @todo Smelly exception for hash's first-level array in RubyVM
    unless [:ARRAY, :LIST].include?(top.type) && parent == :HASH
      range = Range.from_node(top)
      if result.empty? || range.start.line > result.last.start.line
        result.push range unless range.ending.line - range.start.line < 2
      end
    end
  end
  top.children.each do |child|
    inner_folding_ranges(child, result, top.type)
  end
end
inner_tree_at(node, position, stack) click to toggle source

@param node [Parser::AST::Node] @param position [Position] @param stack [Array<Parser::AST::Node>] @return [void]

# File lib/solargraph/source.rb, line 434
def inner_tree_at node, position, stack
  return if node.nil?
  # here = Range.from_to(node.loc.expression.line, node.loc.expression.column, node.loc.expression.last_line, node.loc.expression.last_column)
  here = Range.from_node(node)
  if here.contain?(position) || colonized(here, position, node)
    stack.unshift node
    node.children.each do |c|
      next unless Parser.is_ast_node?(c)
      next if !Parser.rubyvm? && c.loc.expression.nil?
      inner_tree_at(c, position, stack)
    end
  end
end
string_nodes() click to toggle source

@return [Array<Parser::AST::Node>]

# File lib/solargraph/source.rb, line 387
def string_nodes
  @string_nodes ||= string_nodes_in(@node)
end
string_nodes_in(n) click to toggle source

@param n [Parser::AST::Node] @return [Array<Parser::AST::Node>]

# File lib/solargraph/source.rb, line 418
def string_nodes_in n
  result = []
  if Parser.is_ast_node?(n)
    if n.type == :str || n.type == :dstr || n.type == :STR || n.type == :DSTR
      result.push n
    else
      n.children.each{ |c| result.concat string_nodes_in(c) }
    end
  end
  result
end
stringified_comments() click to toggle source

A hash of line numbers and their associated comments.

@return [Hash{Integer => Array<String>}]

# File lib/solargraph/source.rb, line 382
def stringified_comments
  @stringified_comments ||= {}
end
stringify_comment_array(comments) click to toggle source

Get a string representation of an array of comments.

@param comments [String] @return [String]

# File lib/solargraph/source.rb, line 359
def stringify_comment_array comments
  ctxt = String.new('')
  started = false
  skip = nil
  comments.lines.each { |l|
    # Trim the comment and minimum leading whitespace
    p = l.gsub(/^#+/, '')
    if p.strip.empty?
      next unless started
      ctxt.concat p
    else
      here = p.index(/[^ \t]/)
      skip = here if skip.nil? || here < skip
      ctxt.concat p[skip..-1]
    end
    started = true
  }
  ctxt
end