Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## HEAD (unreleased)

- Handle if/else with comment or empty line in branch (https://github.com/ruby/syntax_suggest/pull/193)
- Use `SYNTAX_SUGGEST_DEBUG` instead of `DEBUG` env var value in timeout warning message (https://github.com/ruby/syntax_suggest/pull/194)
- Reduce line output for increased clarity (https://github.com/ruby/syntax_suggest/pull/190)

Expand Down
265 changes: 69 additions & 196 deletions lib/syntax_suggest/around_block_scan.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require_relative "scan_history"

module SyntaxSuggest
# This class is useful for exploring contents before and after
# a block
Expand All @@ -24,22 +26,17 @@ module SyntaxSuggest
# 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
class AroundBlockScan
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
@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

# When using this flag, `scan_while` will
Expand Down Expand Up @@ -89,150 +86,35 @@ def stop_after_kw
# stopping if we've found a keyword/end mis-match in one direction
# or the other.
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
stop_next_up = false
stop_next_down = false

if index && index > after_index
@after_index = index
end
self
end
@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?

# 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
#
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
if @stop_after_kw && kw_count > end_count
stop_next_up = true
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
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?

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

# Shows the context around code provided by "falling" indentation
#
# Converts:
#
# it "foo" do
#
# into:
#
# class OH
# def hello
# it "foo" do
# end
# end
#
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
self
end

# Scanning is intentionally conservative because
Expand Down Expand Up @@ -266,39 +148,46 @@ def lookahead_balance_one_line

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
@scanner.commit_if_changed # Rollback point if we don't find anything to optimize

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

@after_index = next_down.index
# 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

# Finds code lines at the same or greater indentation and adds them
# to the block
def scan_neighbors_not_empty
scan_while { |line| line.not_empty? && line.indent >= @orig_indent }
end

# Returns the next line to be scanned above the current block.
# Returns `nil` if at the top of the document already
def next_up
@code_lines[before_index.pred]
end

# Returns the next line to be scanned below the current block.
# Returns `nil` if at the bottom of the document already
def next_down
@code_lines[after_index.next]
@target_indent = @orig_indent
scan_while { |line| line.not_empty? && line.indent >= @target_indent }
end

# Scan blocks based on indentation of next line above/below block
Expand All @@ -310,11 +199,12 @@ def next_down
# the `def/end` lines surrounding a method.
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 }
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
Expand All @@ -331,29 +221,12 @@ def code_block
# Returns the lines matched by the current scan as an
# array of CodeLines
def lines
@code_lines[before_index..after_index]
end

# Gives the index of the first line currently scanned
def before_index
@before_index ||= @orig_before_index
end

# Gives the index of the last line currently scanned
def after_index
@after_index ||= @orig_after_index
end

# Returns an array of all the CodeLines that exist before
# the currently scanned block
private def before_lines
@code_lines[0...before_index] || []
@scanner.lines
end

# Returns an array of all the CodeLines that exist after
# the currently scanned block
private def after_lines
@code_lines[after_index.next..-1] || []
# Managable rspec errors
def inspect
"#<#{self.class}:0x0000123843lol >"
end
end
end
16 changes: 11 additions & 5 deletions lib/syntax_suggest/block_expand.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,14 @@ def call(block)
# they can expand to capture more code up and down). It does this conservatively
# as there's no undo (currently).
def expand_indent(block)
AroundBlockScan.new(code_lines: @code_lines, block: block)
now = AroundBlockScan.new(code_lines: @code_lines, block: block)
.force_add_hidden
.stop_after_kw
.scan_adjacent_indent
.code_block

now.lookahead_balance_one_line

now.code_block
end

# A neighbor is code that is at or above the current indent line.
Expand Down Expand Up @@ -125,17 +128,20 @@ def expand_indent(block)
#
# We try to resolve this edge case with `lookahead_balance_one_line` below.
def expand_neighbors(block)
neighbors = AroundBlockScan.new(code_lines: @code_lines, block: block)
now = AroundBlockScan.new(code_lines: @code_lines, block: block)

# Initial scan
now
.force_add_hidden
.stop_after_kw
.scan_neighbors_not_empty

# Slurp up empties
with_empties = neighbors
now
.scan_while { |line| line.empty? }

# If next line is kw and it will balance us, take it
expanded_lines = with_empties
expanded_lines = now
.lookahead_balance_one_line
.lines

Expand Down
Loading