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

Contents can also be filtered using AroundBlockScan#skip

To grab the next surrounding indentation use AroundBlockScan#scan_adjacent_indent

Instance Method Summary collapse

Constructor Details

#initialize(code_lines:, block:) ⇒ AroundBlockScan

Returns a new instance of AroundBlockScan.



31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/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

Instance Method Details

#after_indexObject

Gives the index of the last line currently scanned



361
362
363
# File 'lib/syntax_suggest/around_block_scan.rb', line 361

def after_index
  @after_index ||= @orig_after_index
end

#before_indexObject

Gives the index of the first line currently scanned



356
357
358
# File 'lib/syntax_suggest/around_block_scan.rb', line 356

def before_index
  @before_index ||= @orig_before_index
end

#capture_neighbor_contextObject

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


163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/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
  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
  end

  lines
end

#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



345
346
347
# File 'lib/syntax_suggest/around_block_scan.rb', line 345

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.



63
64
65
66
# File 'lib/syntax_suggest/around_block_scan.rb', line 63

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.



52
53
54
55
# File 'lib/syntax_suggest/around_block_scan.rb', line 52

def force_add_hidden
  @force_add_hidden = true
  self
end

#linesObject

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



351
352
353
# File 'lib/syntax_suggest/around_block_scan.rb', line 351

def lines
  @code_lines[before_index..after_index]
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.



259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'lib/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_downObject

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



300
301
302
# File 'lib/syntax_suggest/around_block_scan.rb', line 300

def next_down
  @code_lines[after_index.next]
end

#next_upObject

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



294
295
296
# File 'lib/syntax_suggest/around_block_scan.rb', line 294

def next_up
  @code_lines[before_index.pred]
end

#on_falling_indentObject

Shows the context around code provided by “falling” indentation

Converts:

it "foo" do

into:

class OH
  def hello
    it "foo" do
  end
end


218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/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_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.



311
312
313
314
315
316
317
318
319
320
# File 'lib/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_emptyObject

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



288
289
290
# File 'lib/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_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.



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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/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

#start_at_next_lineObject

TODO: Doc or delete

I don’t remember why this is needed, but it’s called in code_context. It’s related to the implementation of capture_neighbor_context somehow and that display improvement is only triggered when there’s one visible line

I think the primary purpose is to not include the current line in the logic evaluation of capture_neighbor_context. If that’s true, then we should fix that method to handle this logic instead of only using it in one place and together.



332
333
334
335
336
337
338
# File 'lib/syntax_suggest/around_block_scan.rb', line 332

def start_at_next_line
  before_index
  after_index
  @before_index -= 1
  @after_index += 1
  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.



76
77
78
79
# File 'lib/syntax_suggest/around_block_scan.rb', line 76

def stop_after_kw
  @stop_after_kw = true
  self
end