diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a3b949..2985d45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/lib/syntax_suggest/around_block_scan.rb b/lib/syntax_suggest/around_block_scan.rb index fe63470..ce00431 100644 --- a/lib/syntax_suggest/around_block_scan.rb +++ b/lib/syntax_suggest/around_block_scan.rb @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/lib/syntax_suggest/block_expand.rb b/lib/syntax_suggest/block_expand.rb index 8431d15..e9b486c 100644 --- a/lib/syntax_suggest/block_expand.rb +++ b/lib/syntax_suggest/block_expand.rb @@ -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. @@ -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 diff --git a/lib/syntax_suggest/capture/before_after_keyword_ends.rb b/lib/syntax_suggest/capture/before_after_keyword_ends.rb new file mode 100644 index 0000000..f53c57a --- /dev/null +++ b/lib/syntax_suggest/capture/before_after_keyword_ends.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +module SyntaxSuggest + module Capture + # 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 + # + # + # Example: + # + # lines = BeforeAfterKeywordEnds.new( + # block: block, + # code_lines: code_lines + # ).call() + # + class BeforeAfterKeywordEnds + def initialize(code_lines:, block:) + @scanner = ScanHistory.new(code_lines: code_lines, block: block) + @original_indent = block.current_indent + end + + def call + lines = [] + + @scanner.scan( + up: ->(line, kw_count, end_count) { + next true if line.empty? + break if line.indent < @original_indent + next true if line.indent != @original_indent + + # If we're going up and have one complete kw/end pair, stop + if kw_count != 0 && kw_count == end_count + lines << line + break + end + + lines << line if line.is_kw? || line.is_end? + true + }, + down: ->(line, kw_count, end_count) { + next true if line.empty? + break if line.indent < @original_indent + next true if line.indent != @original_indent + + # if we're going down and have one complete kw/end pair,stop + if kw_count != 0 && kw_count == end_count + lines << line + break + end + + lines << line if line.is_kw? || line.is_end? + true + } + ) + @scanner.stash_changes + + lines + end + end + end +end diff --git a/lib/syntax_suggest/capture/falling_indent_lines.rb b/lib/syntax_suggest/capture/falling_indent_lines.rb new file mode 100644 index 0000000..1e046b2 --- /dev/null +++ b/lib/syntax_suggest/capture/falling_indent_lines.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module SyntaxSuggest + module Capture + # Shows the context around code provided by "falling" indentation + # + # If this is the original code lines: + # + # class OH + # def hello + # it "foo" do + # end + # end + # + # And this is the line that is captured + # + # it "foo" do + # + # It will yield its surrounding context: + # + # class OH + # def hello + # end + # end + # + # Example: + # + # FallingIndentLines.new( + # block: block, + # code_lines: @code_lines + # ).call do |line| + # @lines_to_output << line + # end + # + class FallingIndentLines + def initialize(code_lines:, block:) + @lines = nil + @scanner = ScanHistory.new(code_lines: code_lines, block: block) + @original_indent = block.current_indent + end + + def call(&yieldable) + last_indent_up = @original_indent + last_indent_down = @original_indent + + @scanner.commit_if_changed + @scanner.scan( + up: ->(line, _, _) { + next true if line.empty? + + if line.indent < last_indent_up + yieldable.call(line) + last_indent_up = line.indent + end + true + }, + down: ->(line, _, _) { + next true if line.empty? + + if line.indent < last_indent_down + yieldable.call(line) + last_indent_down = line.indent + end + true + } + ) + @scanner.stash_changes + end + end + end +end diff --git a/lib/syntax_suggest/capture_code_context.rb b/lib/syntax_suggest/capture_code_context.rb index a618b2e..6dc7047 100644 --- a/lib/syntax_suggest/capture_code_context.rb +++ b/lib/syntax_suggest/capture_code_context.rb @@ -1,5 +1,13 @@ # frozen_string_literal: true +module SyntaxSuggest + module Capture + end +end + +require_relative "capture/falling_indent_lines" +require_relative "capture/before_after_keyword_ends" + module SyntaxSuggest # Turns a "invalid block(s)" into useful context # @@ -55,6 +63,10 @@ def call capture_falling_indent(block) end + sorted_lines + end + + def sorted_lines @lines_to_output.select!(&:not_empty?) @lines_to_output.uniq! @lines_to_output.sort! @@ -77,10 +89,10 @@ def call # end # def capture_falling_indent(block) - AroundBlockScan.new( + Capture::FallingIndentLines.new( block: block, code_lines: @code_lines - ).on_falling_indent do |line| + ).call do |line| @lines_to_output << line end end @@ -115,8 +127,10 @@ def capture_falling_indent(block) def capture_before_after_kws(block) return unless block.visible_lines.count == 1 - around_lines = AroundBlockScan.new(code_lines: @code_lines, block: block) - .capture_neighbor_context + around_lines = Capture::BeforeAfterKeywordEnds.new( + code_lines: @code_lines, + block: block + ).call around_lines -= block.lines diff --git a/lib/syntax_suggest/scan_history.rb b/lib/syntax_suggest/scan_history.rb new file mode 100644 index 0000000..d15597c --- /dev/null +++ b/lib/syntax_suggest/scan_history.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +module SyntaxSuggest + # Scans up/down from the given block + # + # You can try out a change, stash it, or commit it to save for later + # + # Example: + # + # scanner = ScanHistory.new(code_lines: code_lines, block: block) + # scanner.scan( + # up: ->(_, _, _) { true }, + # down: ->(_, _, _) { true } + # ) + # scanner.changed? # => true + # expect(scanner.lines).to eq(code_lines) + # + # scanner.stash_changes + # + # expect(scanner.lines).to_not eq(code_lines) + class ScanHistory + attr_reader :before_index, :after_index + + def initialize(code_lines:, block:) + @code_lines = code_lines + @history = [block] + refresh_index + end + + def commit_if_changed + if changed? + @history << CodeBlock.new(lines: @code_lines[before_index..after_index]) + end + + self + end + + # Discards any changes that have not been committed + def stash_changes + refresh_index + self + end + + # Discard changes that have not been committed and revert the last commit + # + # Cannot revert the first commit + def revert_last_commit + if @history.length > 1 + @history.pop + refresh_index + end + + self + end + + def changed? + @before_index != current.lines.first.index || + @after_index != current.lines.last.index + end + + # Iterates up and down + # + # Returns line, kw_count, end_count for each iteration + def scan(up:, down:) + kw_count = 0 + end_count = 0 + + up_index = before_lines.reverse_each.take_while do |line| + kw_count += 1 if line.is_kw? + end_count += 1 if line.is_end? + up.call(line, kw_count, end_count) + end.last&.index + + kw_count = 0 + end_count = 0 + + down_index = after_lines.each.take_while do |line| + kw_count += 1 if line.is_kw? + end_count += 1 if line.is_end? + down.call(line, kw_count, end_count) + end.last&.index + + @before_index = if up_index && up_index < @before_index + up_index + else + @before_index + end + + @after_index = if down_index && down_index > @after_index + down_index + else + @after_index + end + + self + end + + def next_up + return nil if @before_index <= 0 + + @code_lines[@before_index - 1] + end + + def next_down + return nil if @after_index >= @code_lines.length + + @code_lines[@after_index + 1] + end + + def lines + @code_lines[@before_index..@after_index] + end + + private def before_lines + @code_lines[0...@before_index] || [] + 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] || [] + end + + private def current + @history.last + end + + private def refresh_index + @before_index = current.lines.first.index + @after_index = current.lines.last.index + self + end + end +end diff --git a/spec/integration/syntax_suggest_spec.rb b/spec/integration/syntax_suggest_spec.rb index f1ceac2..64dafab 100644 --- a/spec/integration/syntax_suggest_spec.rb +++ b/spec/integration/syntax_suggest_spec.rb @@ -21,10 +21,11 @@ module SyntaxSuggest filename: file ) end - debug_display(io.string) - debug_display(benchmark) end + debug_display(io.string) + debug_display(benchmark) + expect(io.string).to include(<<~'EOM') 6 class SyntaxTree < Ripper 170 def self.parse(source) @@ -203,5 +204,36 @@ def bark > 4 end EOM end + + it "empty else" do + source = <<~'EOM' + class Foo + def foo + if cond? + foo + else + + end + end + + # ... + + def bar + if @recv + end_is_missing_here + end + end + EOM + + io = StringIO.new + SyntaxSuggest.call( + io: io, + source: source + ) + out = io.string + expect(out).to include(<<~EOM) + end_is_missing_here + EOM + end end end diff --git a/spec/unit/around_block_scan_spec.rb b/spec/unit/around_block_scan_spec.rb index 88d973e..d675644 100644 --- a/spec/unit/around_block_scan_spec.rb +++ b/spec/unit/around_block_scan_spec.rb @@ -104,8 +104,8 @@ def foo expand = AroundBlockScan.new(code_lines: code_lines, block: block) expand.scan_while { true } - expect(expand.before_index).to eq(0) - expect(expand.after_index).to eq(6) + expect(expand.lines.first.index).to eq(0) + expect(expand.lines.last.index).to eq(6) expect(expand.code_block.to_s).to eq(source_string) end diff --git a/spec/unit/capture/before_after_keyword_ends_spec.rb b/spec/unit/capture/before_after_keyword_ends_spec.rb new file mode 100644 index 0000000..02d9be4 --- /dev/null +++ b/spec/unit/capture/before_after_keyword_ends_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require_relative "../../spec_helper" + +module SyntaxSuggest + RSpec.describe Capture::BeforeAfterKeywordEnds do + it "before after keyword ends" do + source = <<~'EOM' + def nope + print 'not me' + end + + def lol + print 'lol' + end + + def hello # 8 + + def yolo + print 'haha' + end + + def nada + print 'nope' + end + EOM + + code_lines = CleanDocument.new(source: source).call.lines + block = CodeBlock.new(lines: code_lines[8]) + + expect(block.to_s).to include("def hello") + + lines = Capture::BeforeAfterKeywordEnds.new( + block: block, + code_lines: code_lines + ).call + lines.sort! + + expect(lines.join).to include(<<~'EOM') + def lol + end + def yolo + end + EOM + end + end +end diff --git a/spec/unit/capture/falling_indent_lines_spec.rb b/spec/unit/capture/falling_indent_lines_spec.rb new file mode 100644 index 0000000..61d1642 --- /dev/null +++ b/spec/unit/capture/falling_indent_lines_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require_relative "../../spec_helper" + +module SyntaxSuggest + RSpec.describe Capture::FallingIndentLines do + it "on_falling_indent" do + source = <<~'EOM' + class OH + def lol + print 'lol + end + + def hello + it "foo" do + end + + def yolo + print 'haha' + end + end + EOM + + code_lines = CleanDocument.new(source: source).call.lines + block = CodeBlock.new(lines: code_lines[6]) + + lines = [] + Capture::FallingIndentLines.new( + block: block, + code_lines: code_lines + ).call do |line| + lines << line + end + lines.sort! + + expect(lines.join).to eq(<<~'EOM') + class OH + def hello + end + end + EOM + end + end +end diff --git a/spec/unit/capture_code_context_spec.rb b/spec/unit/capture_code_context_spec.rb index 1563149..46f13e8 100644 --- a/spec/unit/capture_code_context_spec.rb +++ b/spec/unit/capture_code_context_spec.rb @@ -4,6 +4,32 @@ module SyntaxSuggest RSpec.describe CaptureCodeContext do + it "capture_before_after_kws two" do + source = <<~'EOM' + class OH + + def hello + + def hai + end + end + EOM + + code_lines = CleanDocument.new(source: source).call.lines + block = CodeBlock.new(lines: code_lines[2]) + + display = CaptureCodeContext.new( + blocks: [block], + code_lines: code_lines + ) + display.capture_before_after_kws(block) + expect(display.sorted_lines.join).to eq(<<~'EOM'.indent(2)) + def hello + def hai + end + EOM + end + it "capture_before_after_kws" do source = <<~'EOM' def sit diff --git a/spec/unit/scan_history_spec.rb b/spec/unit/scan_history_spec.rb new file mode 100644 index 0000000..0e75ac6 --- /dev/null +++ b/spec/unit/scan_history_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require_relative "../spec_helper" + +module SyntaxSuggest + RSpec.describe ScanHistory do + it "retains commits" do + source = <<~'EOM' + class OH # 0 + def lol # 1 + print 'lol # 2 + end # 3 + + def hello # 5 + it "foo" do # 6 + end # 7 + + def yolo # 8 + print 'haha' # 9 + end # 10 + end + EOM + + code_lines = CleanDocument.new(source: source).call.lines + block = CodeBlock.new(lines: code_lines[6]) + + scanner = ScanHistory.new(code_lines: code_lines, block: block) + scanner.scan(up: ->(_, _, _) { true }, down: ->(_, _, _) { true }) + + expect(scanner.changed?).to be_truthy + scanner.commit_if_changed + expect(scanner.changed?).to be_falsey + + expect(scanner.lines).to eq(code_lines) + + scanner.stash_changes # Assert does nothing if changes are already committed + expect(scanner.lines).to eq(code_lines) + + scanner.revert_last_commit + + expect(scanner.lines.join).to eq(code_lines[6].to_s) + end + + it "is stashable" do + source = <<~'EOM' + class OH # 0 + def lol # 1 + print 'lol # 2 + end # 3 + + def hello # 5 + it "foo" do # 6 + end # 7 + + def yolo # 8 + print 'haha' # 9 + end # 10 + end + EOM + + code_lines = CleanDocument.new(source: source).call.lines + block = CodeBlock.new(lines: code_lines[6]) + + scanner = ScanHistory.new(code_lines: code_lines, block: block) + scanner.scan(up: ->(_, _, _) { true }, down: ->(_, _, _) { true }) + + expect(scanner.lines).to eq(code_lines) + expect(scanner.changed?).to be_truthy + expect(scanner.next_up).to be_falsey + expect(scanner.next_down).to be_falsey + + scanner.stash_changes + + expect(scanner.changed?).to be_falsey + + expect(scanner.next_up).to eq(code_lines[5]) + expect(scanner.lines.join).to eq(code_lines[6].to_s) + expect(scanner.next_down).to eq(code_lines[7]) + end + + it "doesnt change if you dont't change it" do + source = <<~'EOM' + class OH # 0 + def lol # 1 + print 'lol # 2 + end # 3 + + def hello # 5 + it "foo" do # 6 + end # 7 + + def yolo # 8 + print 'haha' # 9 + end # 10 + end + EOM + + code_lines = CleanDocument.new(source: source).call.lines + block = CodeBlock.new(lines: code_lines[6]) + + scanner = ScanHistory.new(code_lines: code_lines, block: block) + + lines = scanner.lines + expect(scanner.changed?).to be_falsey + expect(scanner.next_up).to eq(code_lines[5]) + expect(scanner.next_down).to eq(code_lines[7]) + + expect(scanner.stash_changes.lines).to eq(lines) + expect(scanner.revert_last_commit.lines).to eq(lines) + + expect(scanner.scan(up: ->(_, _, _) { false }, down: ->(_, _, _) { false }).lines).to eq(lines) + end + end +end