class SyntaxSuggest::AroundBlockScan

This class is useful for exploring contents before and after a block

It searches above and below the passed in block to match for whatever criteria you give it:

Example:

def dog         # 1
  puts "bark"   # 2
  puts "bark"   # 3
end             # 4

scan = AroundBlockScan.new(
  code_lines: code_lines
  block: CodeBlock.new(lines: code_lines[1])
)

scan.scan_while { true }

puts scan.before_index # => 0
puts scan.after_index  # => 3

Contents can also be filtered using AroundBlockScan#skip

To grab the next surrounding indentation use AroundBlockScan#scan_adjacent_indent

Public Class Methods

new(code_lines:, block:) click to toggle source
# File syntax_suggest/around_block_scan.rb, line 31
def initialize(code_lines:, block:)
  @code_lines = code_lines
  @orig_before_index = block.lines.first.index
  @orig_after_index = block.lines.last.index
  @orig_indent = block.current_indent
  @skip_array = []
  @after_array = []
  @before_array = []
  @stop_after_kw = false

  @force_add_hidden = false
  @force_add_empty = false
end

Public Instance Methods

after_index() click to toggle source

Gives the index of the last line currently scanned

# File syntax_suggest/around_block_scan.rb, line 343
def after_index
  @after_index ||= @orig_after_index
end
before_index() click to toggle source

Gives the index of the first line currently scanned

# File syntax_suggest/around_block_scan.rb, line 338
def before_index
  @before_index ||= @orig_before_index
end
capture_neighbor_context() click to toggle source

Shows surrounding kw/end pairs

The purpose of showing these extra pairs is due to cases of ambiguity when only one visible line is matched.

For example:

1  class Dog
2    def bark
4    def eat
5    end
6  end

In this case either line 2 could be missing an ‘end` or line 4 was an extra line added by mistake (it happens).

When we detect the above problem it shows the issue as only being on line 2

2    def bark

Showing “neighbor” keyword pairs gives extra context:

2    def bark
4    def eat
5    end
# File syntax_suggest/around_block_scan.rb, line 163
def capture_neighbor_context
  lines = []
  kw_count = 0
  end_count = 0
  before_lines.reverse_each do |line|
    next if line.empty?
    break if line.indent < @orig_indent
    next if line.indent != @orig_indent

    kw_count += 1 if line.is_kw?
    end_count += 1 if line.is_end?
    if kw_count != 0 && kw_count == end_count
      lines << line
      break
    end

    lines << line if line.is_kw? || line.is_end?
  end

  lines.reverse!

  kw_count = 0
  end_count = 0
  after_lines.each do |line|
    next if line.empty?
    break if line.indent < @orig_indent
    next if line.indent != @orig_indent

    kw_count += 1 if line.is_kw?
    end_count += 1 if line.is_end?
    if kw_count != 0 && kw_count == end_count
      lines << line
      break
    end

    lines << line if line.is_kw? || line.is_end?
  end

  lines
end
code_block() click to toggle source

Return the currently matched lines as a ‘CodeBlock`

When a ‘CodeBlock` is created it will gather metadata about itself, so this is not a free conversion. Avoid allocating more CodeBlock’s than needed

# File syntax_suggest/around_block_scan.rb, line 327
def code_block
  CodeBlock.new(lines: lines)
end
force_add_empty() click to toggle source

When using this flag, ‘scan_while` will bypass the block it’s given and always add a line that responds truthy to ‘CodeLine#empty?`

Empty lines contain no code, only whitespace such as leading spaces a newline.

# File syntax_suggest/around_block_scan.rb, line 63
def force_add_empty
  @force_add_empty = true
  self
end
force_add_hidden() click to toggle source

When using this flag, ‘scan_while` will bypass the block it’s given and always add a line that responds truthy to ‘CodeLine#hidden?`

Lines are hidden when they’ve been evaluated by the parser as part of a block and found to contain valid code.

# File syntax_suggest/around_block_scan.rb, line 52
def force_add_hidden
  @force_add_hidden = true
  self
end
lines() click to toggle source

Returns the lines matched by the current scan as an array of CodeLines

# File syntax_suggest/around_block_scan.rb, line 333
def lines
  @code_lines[before_index..after_index]
end
lookahead_balance_one_line() click to toggle source

Scanning is intentionally conservative because we have no way of rolling back an agressive block (at this time)

If a block was stopped for some trivial reason, (like an empty line) but the next line would have caused it to be balanced then we can check that condition and grab just one more line either up or down.

For example, below if we’re scanning up, line 2 might cause the scanning to stop. This is because empty lines might denote logical breaks where the user intended to chunk code which is a good place to stop and check validity. Unfortunately it also means we might have a “dangling” keyword or end.

1 def bark
2
3 end

If lines 2 and 3 are in the block, then when this method is run it would see it is unbalanced, but that acquiring line 1 would make it balanced, so that’s what it does.

# File syntax_suggest/around_block_scan.rb, line 259
def lookahead_balance_one_line
  kw_count = 0
  end_count = 0
  lines.each do |line|
    kw_count += 1 if line.is_kw?
    end_count += 1 if line.is_end?
  end

  return self if kw_count == end_count # nothing to balance

  # More ends than keywords, check if we can balance expanding up
  if (end_count - kw_count) == 1 && next_up
    return self unless next_up.is_kw?
    return self unless next_up.indent >= @orig_indent

    @before_index = next_up.index

  # More keywords than ends, check if we can balance by expanding down
  elsif (kw_count - end_count) == 1 && next_down
    return self unless next_down.is_end?
    return self unless next_down.indent >= @orig_indent

    @after_index = next_down.index
  end
  self
end
next_down() click to toggle source

Returns the next line to be scanned below the current block. Returns ‘nil` if at the bottom of the document already

# File syntax_suggest/around_block_scan.rb, line 300
def next_down
  @code_lines[after_index.next]
end
next_up() click to toggle source

Returns the next line to be scanned above the current block. Returns ‘nil` if at the top of the document already

# File syntax_suggest/around_block_scan.rb, line 294
def next_up
  @code_lines[before_index.pred]
end
on_falling_indent() { |line| ... } click to toggle source

Shows the context around code provided by “falling” indentation

Converts:

it "foo" do

into:

class OH
  def hello
    it "foo" do
  end
end
# File syntax_suggest/around_block_scan.rb, line 218
def on_falling_indent
  last_indent = @orig_indent
  before_lines.reverse_each do |line|
    next if line.empty?
    if line.indent < last_indent
      yield line
      last_indent = line.indent
    end
  end

  last_indent = @orig_indent
  after_lines.each do |line|
    next if line.empty?
    if line.indent < last_indent
      yield line
      last_indent = line.indent
    end
  end
end
scan_adjacent_indent() click to toggle source

Scan blocks based on indentation of next line above/below block

Determines indentaion of the next line above/below the current block.

Normally this is called when a block has expanded to capture all “neighbors” at the same (or greater) indentation and needs to expand out. For example the ‘def/end` lines surrounding a method.

# File syntax_suggest/around_block_scan.rb, line 311
def scan_adjacent_indent
  before_after_indent = []
  before_after_indent << (next_up&.indent || 0)
  before_after_indent << (next_down&.indent || 0)

  indent = before_after_indent.min
  scan_while { |line| line.not_empty? && line.indent >= indent }

  self
end
scan_neighbors_not_empty() click to toggle source

Finds code lines at the same or greater indentation and adds them to the block

# File syntax_suggest/around_block_scan.rb, line 288
def scan_neighbors_not_empty
  scan_while { |line| line.not_empty? && line.indent >= @orig_indent }
end
scan_while() { |line| ... } click to toggle source

Main work method

The scan_while method takes a block that yields lines above and below the block. If the yield returns true, the @before_index or @after_index are modified to include the matched line.

In addition to yielding individual lines, the internals of this object give a mini DSL to handle common situations such as stopping if we’ve found a keyword/end mis-match in one direction or the other.

# File syntax_suggest/around_block_scan.rb, line 91
def scan_while
  stop_next = false
  kw_count = 0
  end_count = 0
  index = before_lines.reverse_each.take_while do |line|
    next false if stop_next
    next true if @force_add_hidden && line.hidden?
    next true if @force_add_empty && line.empty?

    kw_count += 1 if line.is_kw?
    end_count += 1 if line.is_end?
    if @stop_after_kw && kw_count > end_count
      stop_next = true
    end

    yield line
  end.last&.index

  if index && index < before_index
    @before_index = index
  end

  stop_next = false
  kw_count = 0
  end_count = 0
  index = after_lines.take_while do |line|
    next false if stop_next
    next true if @force_add_hidden && line.hidden?
    next true if @force_add_empty && line.empty?

    kw_count += 1 if line.is_kw?
    end_count += 1 if line.is_end?
    if @stop_after_kw && end_count > kw_count
      stop_next = true
    end

    yield line
  end.last&.index

  if index && index > after_index
    @after_index = index
  end
  self
end
stop_after_kw() click to toggle source

Tells ‘scan_while` to look for mismatched keyword/end-s

When scanning up, if we see more keywords then end-s it will stop. This might happen when scanning outside of a method body. the first scan line up would be a keyword and this setting would trigger a stop.

When scanning down, stop if there are more end-s than keywords.

# File syntax_suggest/around_block_scan.rb, line 76
def stop_after_kw
  @stop_after_kw = true
  self
end