123456789_123456789_123456789_123456789_123456789_

Class: SyntaxSuggest::CodeLine

Relationships & Source Files
Inherits: Object
Defined in: lib/syntax_suggest/code_line.rb

Overview

Represents a single line of code of a given source file

This object contains metadata about the line such as amount of indentation, if it is empty or not, and lexical data, such as if it has an end or a keyword in it.

Visibility of lines can be toggled off. Marking a line as invisible indicates that it should not be used for syntax checks. It's functionally the same as commenting it out.

Example:

line = CodeLine.from_source("def foo\n").first
line.number => 1
line.empty? # => false
line.visible? # => true
line.mark_invisible
line.visible? # => false

Constant Summary

Class Method Summary

Instance Attribute Summary

Instance Method Summary

Constructor Details

.new(line:, index:, tokens:, consecutive:) ⇒ CodeLine

[ GitHub ]

  
# File 'lib/syntax_suggest/code_line.rb', line 75

def initialize(line:, index:, tokens:, consecutive:)
  @tokens = tokens
  @line = line
  @index = index
  @consecutive = consecutive
  @original = line
  @line_number = @index + 1
  strip_line = line.dup
  strip_line.lstrip!

  @indent = if (@empty = strip_line.empty?)
    line.length - 1 # Newline removed from strip_line is not "whitespace"
  else
    line.length - strip_line.length
  end

  set_kw_end
end

Class Method Details

.clean_comments!(source, comments)

Remove comments that apear on their own in source. They will never be the cause of syntax errors and are just visual noise. Example:

source = +<<~RUBY
# Comment-only line
foo # Inline comment
RUBY
CodeLine.clean_comments!(source, Prism.parse(source).comments)
source # => "\nfoo # Inline comment\n"
[ GitHub ]

  
# File 'lib/syntax_suggest/code_line.rb', line 65

def self.clean_comments!(source, comments)
  # Iterate backwards since we are modifying the source in place and must preserve
  # the offsets. Prism comments are sorted by their location in the source.
  comments.reverse_each do |comment|
    next if comment.trailing?
    source.bytesplice(comment.location.start_offset, comment.location.length, "")
  end
end

.from_source(source)

Returns an array of CodeLine objects from the source string

[ GitHub ]

  
# File 'lib/syntax_suggest/code_line.rb', line 29

def self.from_source(source)
  source = +source
  parse_result = Prism.parse_lex(source)
  ast, tokens = parse_result.value

  clean_comments!(source, parse_result.comments)

  visitor = Visitor.new
  visitor.visit(ast)
  tokens.sort_by! { |token, _state| token.location.start_line }

  prev_token = nil
  tokens.map! do |token, _state|
    prev_token = Token.new(token, prev_token, visitor)
  end

  tokens_for_line = tokens.each_with_object(Hash.new { |h, k| h[k] = [] }) { |token, hash| hash[token.line] << token }
  source.lines.map.with_index do |line, index|
    CodeLine.new(
      line: line,
      index: index,
      tokens: tokens_for_line[index + 1],
      consecutive: visitor.consecutive_lines.include?(index + 1)
    )
  end
end

Instance Attribute Details

#consecutive?Boolean (readonly)

Can this line be logically joined together with the following line? Determined by walking the AST

[ GitHub ]

  
# File 'lib/syntax_suggest/code_line.rb', line 191

def consecutive?
  @consecutive
end

#empty?Boolean (readonly)

An empty? line is one that was originally left empty in the source code, while a "hidden" line is one that we've since marked as "invisible"

[ GitHub ]

  
# File 'lib/syntax_suggest/code_line.rb', line 149

def empty?
  @empty
end

#hidden?Boolean (readonly)

Opposite or #visible? (note: different than #empty?)

[ GitHub ]

  
# File 'lib/syntax_suggest/code_line.rb', line 142

def hidden?
  !visible?
end

#indent (readonly)

[ GitHub ]

  
# File 'lib/syntax_suggest/code_line.rb', line 74

attr_reader :line, :index, :tokens, :line_number, :indent

#index (readonly)

[ GitHub ]

  
# File 'lib/syntax_suggest/code_line.rb', line 74

attr_reader :line, :index, :tokens, :line_number, :indent

#is_end?Boolean (readonly)

Returns true if the code line is determined to contain an end keyword

[ GitHub ]

  
# File 'lib/syntax_suggest/code_line.rb', line 121

def is_end?
  @is_end
end

#is_kw?Boolean (readonly)

Returns true if the code line is determined to contain a keyword that matches with an end

For example: def, do, begin, ensure, etc.

[ GitHub ]

  
# File 'lib/syntax_suggest/code_line.rb', line 115

def is_kw?
  @is_kw
end

#line (readonly)

[ GitHub ]

  
# File 'lib/syntax_suggest/code_line.rb', line 74

attr_reader :line, :index, :tokens, :line_number, :indent

#line_number (readonly) Also known as: #number

[ GitHub ]

  
# File 'lib/syntax_suggest/code_line.rb', line 74

attr_reader :line, :index, :tokens, :line_number, :indent

#not_empty?Boolean (readonly)

Opposite of #empty? (note: different than #visible?)

[ GitHub ]

  
# File 'lib/syntax_suggest/code_line.rb', line 154

def not_empty?
  !empty?
end

#number (readonly)

Alias for #line_number.

[ GitHub ]

  
# File 'lib/syntax_suggest/code_line.rb', line 109

alias_method :number, :line_number

#original (readonly)

When the code line is marked invisible we retain the original value of it's line this is useful for debugging and for showing extra context

DisplayCodeWithLineNumbers will render all lines given to it, not just visible lines, it uses the original method to obtain them.

[ GitHub ]

  
# File 'lib/syntax_suggest/code_line.rb', line 180

attr_reader :original

#tokens (readonly)

[ GitHub ]

  
# File 'lib/syntax_suggest/code_line.rb', line 74

attr_reader :line, :index, :tokens, :line_number, :indent

#trailing_slash?Boolean (readonly)

Determines if the given line has a trailing slash. Simply check if the line contains a backslash after the content of the last token.

lines = CodeLine.from_source(<<~EOM)
  it "foo" \
EOM
expect(lines.first.trailing_slash?).to eq(true)
[ GitHub ]

  
# File 'lib/syntax_suggest/code_line.rb', line 204

def trailing_slash?
  return unless (last = @tokens.last)
  @line.byteindex(TRAILING_SLASH, last.location.end_column) != nil
end

#visible?Boolean (readonly)

Means the line was marked as "invisible" Confusingly, "empty" lines are visible...they just don't contain any source code other than a newline ("\n").

[ GitHub ]

  
# File 'lib/syntax_suggest/code_line.rb', line 137

def visible?
  !line.empty?
end

Instance Method Details

#<=>(other)

Comparison operator, needed for equality and sorting

[ GitHub ]

  
# File 'lib/syntax_suggest/code_line.rb', line 184

def <=>(other)
  index <=> other.index
end

#indent_index

Used for stable sort via indentation level

Ruby's sort is not "stable" meaning that when multiple elements have the same value, they are not guaranteed to return in the same order they were put in.

So when multiple code lines have the same indentation level, they're sorted by their index value which is unique and consistent.

This is mostly needed for consistency of the test suite

[ GitHub ]

  
# File 'lib/syntax_suggest/code_line.rb', line 106

def indent_index
  @indent_index ||= [indent, index]
end

#mark_invisible

Used to hide lines

The search alorithm will group lines into blocks then if those blocks are determined to represent valid code they will be hidden

[ GitHub ]

  
# File 'lib/syntax_suggest/code_line.rb', line 130

def mark_invisible
  @line = ""
end

#set_kw_end (private)

[ GitHub ]

  
# File 'lib/syntax_suggest/code_line.rb', line 209

private def set_kw_end
  kw_count = 0
  end_count = 0

  @tokens.each do |token|
    kw_count += 1 if token.is_kw?
    end_count += 1 if token.is_end?
  end

  @is_kw = (kw_count - end_count) > 0
  @is_end = (end_count - kw_count) > 0
end

#to_s

Renders the given line

Also allows us to represent source code as an array of code lines.

When we have an array of code line elements calling join on the array will call to_s on each element, which essentially converts it back into it's original source string.

[ GitHub ]

  
# File 'lib/syntax_suggest/code_line.rb', line 167

def to_s
  line
end