Class: SyntaxSuggest::AroundBlockScan

Inherits:
Object
  • Object
show all
Defined in:
lib/syntax_suggest/around_block_scan.rb

Overview

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

Instance Method Summary collapse

Constructor Details

#initialize(code_lines:, block:) ⇒ AroundBlockScan

Returns a new instance of AroundBlockScan.



30
31
32
33
34
35
36
37
38
39
40
# File 'lib/syntax_suggest/around_block_scan.rb', line 30

def initialize(code_lines:, block:)
  @code_lines = code_lines
  @orig_indent = block.current_indent

  @stop_after_kw = false
  @force_add_empty = false
  @force_add_hidden = false
  @target_indent = nil

  @scanner = ScanHistory.new(code_lines: code_lines, block: block)
end

Instance Method Details

#code_blockObject

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



217
218
219
# File 'lib/syntax_suggest/around_block_scan.rb', line 217

def code_block
  CodeBlock.new(lines: lines)
end

#force_add_emptyObject

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.



60
61
62
63
# File 'lib/syntax_suggest/around_block_scan.rb', line 60

def force_add_empty
  @force_add_empty = true
  self
end

#force_add_hiddenObject

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.



49
50
51
52
# File 'lib/syntax_suggest/around_block_scan.rb', line 49

def force_add_hidden
  @force_add_hidden = true
  self
end

#inspectObject

Managable rspec errors



228
229
230
# File 'lib/syntax_suggest/around_block_scan.rb', line 228

def inspect
  "#<#{self.class}:0x0000123843lol >"
end

#linesObject

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



223
224
225
# File 'lib/syntax_suggest/around_block_scan.rb', line 223

def lines
  @scanner.lines
end

#lookahead_balance_one_lineObject

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.



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/syntax_suggest/around_block_scan.rb', line 141

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

  @scanner.commit_if_changed # Rollback point if we don't find anything to optimize

  # Try to eat up empty lines
  @scanner.scan(
    up: ->(line, _, _) { line.hidden? || line.empty? },
    down: ->(line, _, _) { line.hidden? || line.empty? }
  )

  # More ends than keywords, check if we can balance expanding up
  next_up = @scanner.next_up
  next_down = @scanner.next_down
  case end_count - kw_count
  when 1
    if next_up&.is_kw? && next_up.indent >= @target_indent
      @scanner.scan(
        up: ->(line, _, _) { line == next_up },
        down: ->(line, _, _) { false }
      )
      @scanner.commit_if_changed
    end
  when -1
    if next_down&.is_end? && next_down.indent >= @target_indent
      @scanner.scan(
        up: ->(line, _, _) { false },
        down: ->(line, _, _) { line == next_down }
      )
      @scanner.commit_if_changed
    end
  end
  # Rollback any uncommitted changes
  @scanner.stash_changes

  self
end

#scan_adjacent_indentObject

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.



200
201
202
203
204
205
206
207
208
209
210
# File 'lib/syntax_suggest/around_block_scan.rb', line 200

def scan_adjacent_indent
  before_after_indent = []

  before_after_indent << (@scanner.next_up&.indent || 0)
  before_after_indent << (@scanner.next_down&.indent || 0)

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

  self
end

#scan_neighbors_not_emptyObject

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



188
189
190
191
# File 'lib/syntax_suggest/around_block_scan.rb', line 188

def scan_neighbors_not_empty
  @target_indent = @orig_indent
  scan_while { |line| line.not_empty? && line.indent >= @target_indent }
end

#scan_whileObject

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.



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/syntax_suggest/around_block_scan.rb', line 88

def scan_while
  stop_next_up = false
  stop_next_down = false

  @scanner.scan(
    up: ->(line, kw_count, end_count) {
      next false if stop_next_up
      next true if @force_add_hidden && line.hidden?
      next true if @force_add_empty && line.empty?

      if @stop_after_kw && kw_count > end_count
        stop_next_up = true
      end

      yield line
    },
    down: ->(line, kw_count, end_count) {
      next false if stop_next_down
      next true if @force_add_hidden && line.hidden?
      next true if @force_add_empty && line.empty?

      if @stop_after_kw && end_count > kw_count
        stop_next_down = true
      end

      yield line
    }
  )

  self
end

#stop_after_kwObject

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.



73
74
75
76
# File 'lib/syntax_suggest/around_block_scan.rb', line 73

def stop_after_kw
  @stop_after_kw = true
  self
end