summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHiroshi SHIBATA <hsbt@ruby-lang.org>2023-06-01 17:05:35 +0900
committernagachika <nagachika@ruby-lang.org>2023-08-20 13:26:54 +0900
commite517ba2e5b6434e7d2370de7703f481dcb8eecba (patch)
treefe7484d14beb10c371bde4b0bc6ade32b0f32ea0
parent0c908fa681271f13750aa64420203f1a58fa03fe (diff)
Bump up syntax_suggest-1.1.0
-rw-r--r--lib/syntax_suggest/api.rb6
-rw-r--r--lib/syntax_suggest/around_block_scan.rb290
-rw-r--r--lib/syntax_suggest/block_expand.rb111
-rw-r--r--lib/syntax_suggest/capture/before_after_keyword_ends.rb85
-rw-r--r--lib/syntax_suggest/capture/falling_indent_lines.rb71
-rw-r--r--lib/syntax_suggest/capture_code_context.rb24
-rw-r--r--lib/syntax_suggest/clean_document.rb6
-rw-r--r--lib/syntax_suggest/code_line.rb8
-rw-r--r--lib/syntax_suggest/core_ext.rb6
-rw-r--r--lib/syntax_suggest/parse_blocks_from_indent_line.rb4
-rw-r--r--lib/syntax_suggest/scan_history.rb134
-rw-r--r--lib/syntax_suggest/syntax_suggest.gemspec2
-rw-r--r--lib/syntax_suggest/version.rb2
-rw-r--r--spec/syntax_suggest/integration/exe_cli_spec.rb3
-rw-r--r--spec/syntax_suggest/integration/ruby_command_line_spec.rb18
-rw-r--r--spec/syntax_suggest/integration/syntax_suggest_spec.rb40
-rw-r--r--spec/syntax_suggest/spec_helper.rb6
-rw-r--r--spec/syntax_suggest/unit/around_block_scan_spec.rb12
-rw-r--r--spec/syntax_suggest/unit/block_expand_spec.rb30
-rw-r--r--spec/syntax_suggest/unit/capture/before_after_keyword_ends_spec.rb47
-rw-r--r--spec/syntax_suggest/unit/capture/falling_indent_lines_spec.rb44
-rw-r--r--spec/syntax_suggest/unit/capture_code_context_spec.rb31
-rw-r--r--spec/syntax_suggest/unit/clean_document_spec.rb43
-rw-r--r--spec/syntax_suggest/unit/code_line_spec.rb1
-rw-r--r--spec/syntax_suggest/unit/core_ext_spec.rb34
-rw-r--r--spec/syntax_suggest/unit/display_invalid_blocks_spec.rb2
-rw-r--r--spec/syntax_suggest/unit/scan_history_spec.rb114
27 files changed, 968 insertions, 206 deletions
diff --git a/lib/syntax_suggest/api.rb b/lib/syntax_suggest/api.rb
index 5b725e13d7..74e53c2563 100644
--- a/lib/syntax_suggest/api.rb
+++ b/lib/syntax_suggest/api.rb
@@ -78,7 +78,7 @@ module SyntaxSuggest
code_lines: search.code_lines
).call
rescue Timeout::Error => e
- io.puts "Search timed out SYNTAX_SUGGEST_TIMEOUT=#{timeout}, run with DEBUG=1 for more info"
+ io.puts "Search timed out SYNTAX_SUGGEST_TIMEOUT=#{timeout}, run with SYNTAX_SUGGEST_DEBUG=1 for more info"
io.puts e.backtrace.first(3).join($/)
end
@@ -91,7 +91,9 @@ module SyntaxSuggest
dir = Pathname(dir)
dir.join(time).tap { |path|
path.mkpath
- FileUtils.ln_sf(time, dir.join("last"))
+ alias_dir = dir.join("last")
+ FileUtils.rm_rf(alias_dir) if alias_dir.exist?
+ FileUtils.ln_sf(time, alias_dir)
}
end
diff --git a/lib/syntax_suggest/around_block_scan.rb b/lib/syntax_suggest/around_block_scan.rb
index 2a57d1b19e..ce00431b3a 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,201 +26,207 @@ 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
- @skip_hidden = false
- @skip_empty = 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
+ # 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.
+ def force_add_hidden
+ @force_add_hidden = true
+ self
end
- def skip(name)
- case name
- when :hidden?
- @skip_hidden = true
- when :empty?
- @skip_empty = true
- else
- raise "Unsupported skip #{name}"
- end
+ # 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.
+ def force_add_empty
+ @force_add_empty = true
self
end
+ # 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.
def stop_after_kw
@stop_after_kw = true
self
end
+ # 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.
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 @skip_hidden && line.hidden?
- next true if @skip_empty && line.empty?
+ stop_next_up = false
+ stop_next_down = false
- 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
+ @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 index && index < before_index
- @before_index = index
- end
+ if @stop_after_kw && kw_count > end_count
+ stop_next_up = true
+ 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 @skip_hidden && line.hidden?
- next true if @skip_empty && line.empty?
+ 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?
- 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
+ if @stop_after_kw && end_count > kw_count
+ stop_next_down = true
+ end
- yield line
- end.last&.index
+ yield line
+ }
+ )
- if index && index > after_index
- @after_index = index
- end
self
end
- def capture_neighbor_context
- lines = []
+ # 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.
+ def lookahead_balance_one_line
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
-
+ lines.each do |line|
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
+ 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
-
- lines << line
- end
-
- lines
- 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
+ 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
- end
-
- def scan_neighbors
- scan_while { |line| line.not_empty? && line.indent >= @orig_indent }
- end
+ # Rollback any uncommitted changes
+ @scanner.stash_changes
- def next_up
- @code_lines[before_index.pred]
+ self
end
- def next_down
- @code_lines[after_index.next]
+ # Finds code lines at the same or greater indentation and adds them
+ # to the block
+ def scan_neighbors_not_empty
+ @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
+ #
+ # 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.
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)
- self
- end
+ @target_indent = before_after_indent.min
+ scan_while { |line| line.not_empty? && line.indent >= @target_indent }
- def start_at_next_line
- before_index
- after_index
- @before_index -= 1
- @after_index += 1
self
end
+ # 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
def code_block
CodeBlock.new(lines: lines)
end
+ # Returns the lines matched by the current scan as an
+ # array of CodeLines
def lines
- @code_lines[before_index..after_index]
- end
-
- def before_index
- @before_index ||= @orig_before_index
- end
-
- def after_index
- @after_index ||= @orig_after_index
- end
-
- private def before_lines
- @code_lines[0...before_index] || []
+ @scanner.lines
end
- 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 396b2c3a1a..e9b486c720 100644
--- a/lib/syntax_suggest/block_expand.rb
+++ b/lib/syntax_suggest/block_expand.rb
@@ -35,30 +35,121 @@ module SyntaxSuggest
@code_lines = code_lines
end
+ # Main interface. Expand current indentation, before
+ # expanding to a lower indentation
def call(block)
if (next_block = expand_neighbors(block))
- return next_block
+ next_block
+ else
+ expand_indent(block)
end
-
- expand_indent(block)
end
+ # Expands code to the next lowest indentation
+ #
+ # For example:
+ #
+ # 1 def dog
+ # 2 print "dog"
+ # 3 end
+ #
+ # If a block starts on line 2 then it has captured all it's "neighbors" (code at
+ # the same indentation or higher). To continue expanding, this block must capture
+ # lines one and three which are at a different indentation level.
+ #
+ # This method allows fully expanded blocks to decrease their indentation level (so
+ # 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)
- .skip(:hidden?)
+ 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.
+ #
+ # First we build a block with all neighbors. If we can't go further
+ # then we decrease the indentation threshold and expand via indentation
+ # i.e. `expand_indent`
+ #
+ # Handles two general cases.
+ #
+ # ## Case #1: Check code inside of methods/classes/etc.
+ #
+ # It's important to note, that not everything in a given indentation level can be parsed
+ # as valid code even if it's part of valid code. For example:
+ #
+ # 1 hash = {
+ # 2 name: "richard",
+ # 3 dog: "cinco",
+ # 4 }
+ #
+ # In this case lines 2 and 3 will be neighbors, but they're invalid until `expand_indent`
+ # is called on them.
+ #
+ # When we are adding code within a method or class (at the same indentation level),
+ # use the empty lines to denote the programmer intended logical chunks.
+ # Stop and check each one. For example:
+ #
+ # 1 def dog
+ # 2 print "dog"
+ # 3
+ # 4 hash = {
+ # 5 end
+ #
+ # If we did not stop parsing at empty newlines then the block might mistakenly grab all
+ # the contents (lines 2, 3, and 4) and report them as being problems, instead of only
+ # line 4.
+ #
+ # ## Case #2: Expand/grab other logical blocks
+ #
+ # Once the search algorithm has converted all lines into blocks at a given indentation
+ # it will then `expand_indent`. Once the blocks that generates are expanded as neighbors
+ # we then begin seeing neighbors being other logical blocks i.e. a block's neighbors
+ # may be another method or class (something with keywords/ends).
+ #
+ # For example:
+ #
+ # 1 def bark
+ # 2
+ # 3 end
+ # 4
+ # 5 def sit
+ # 6 end
+ #
+ # In this case if lines 4, 5, and 6 are in a block when it tries to expand neighbors
+ # it will expand up. If it stops after line 2 or 3 it may cause problems since there's a
+ # valid kw/end pair, but the block will be checked without it.
+ #
+ # We try to resolve this edge case with `lookahead_balance_one_line` below.
def expand_neighbors(block)
- expanded_lines = AroundBlockScan.new(code_lines: @code_lines, block: block)
- .skip(:hidden?)
+ now = AroundBlockScan.new(code_lines: @code_lines, block: block)
+
+ # Initial scan
+ now
+ .force_add_hidden
.stop_after_kw
- .scan_neighbors
- .scan_while { |line| line.empty? } # Slurp up empties
+ .scan_neighbors_not_empty
+
+ # Slurp up empties
+ now
+ .scan_while { |line| line.empty? }
+
+ # If next line is kw and it will balance us, take it
+ expanded_lines = now
+ .lookahead_balance_one_line
.lines
+ # Don't allocate a block if it won't be used
+ #
+ # If nothing was taken, return nil to indicate that status
+ # used in `def call` to determine if
+ # we need to expand up/out (`expand_indent`)
if block.lines == expanded_lines
nil
else
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 0000000000..f53c57a4d1
--- /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 0000000000..1e046b2ba5
--- /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 7d6a550612..6dc7047176 100644
--- a/lib/syntax_suggest/capture_code_context.rb
+++ b/lib/syntax_suggest/capture_code_context.rb
@@ -1,6 +1,14 @@
# 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
#
# There are three main phases in the algorithm:
@@ -55,6 +63,10 @@ module SyntaxSuggest
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!
@@ -76,12 +88,11 @@ module SyntaxSuggest
# end
# 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
@@ -116,9 +127,10 @@ module SyntaxSuggest
def capture_before_after_kws(block)
return unless block.visible_lines.count == 1
- around_lines = AroundBlockScan.new(code_lines: @code_lines, block: block)
- .start_at_next_line
- .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/clean_document.rb b/lib/syntax_suggest/clean_document.rb
index b572189259..2c26061bfc 100644
--- a/lib/syntax_suggest/clean_document.rb
+++ b/lib/syntax_suggest/clean_document.rb
@@ -110,7 +110,7 @@ module SyntaxSuggest
@document.join
end
- # Remove comments and whitespace only lines
+ # Remove comments
#
# replace with empty newlines
#
@@ -155,8 +155,10 @@ module SyntaxSuggest
# ).to eq(2)
#
def clean_sweep(source:)
+ # Match comments, but not HEREDOC strings with #{variable} interpolation
+ # https://rubular.com/r/HPwtW9OYxKUHXQ
source.lines.map do |line|
- if line.match?(/^\s*(#[^{].*)?$/) # https://rubular.com/r/LLE10D8HKMkJvs
+ if line.match?(/^\s*#([^{].*|)$/)
$/
else
line
diff --git a/lib/syntax_suggest/code_line.rb b/lib/syntax_suggest/code_line.rb
index dc738ab128..a20f34afa4 100644
--- a/lib/syntax_suggest/code_line.rb
+++ b/lib/syntax_suggest/code_line.rb
@@ -48,12 +48,10 @@ module SyntaxSuggest
strip_line = line.dup
strip_line.lstrip!
- if strip_line.empty?
- @empty = true
- @indent = 0
+ @indent = if (@empty = strip_line.empty?)
+ line.length - 1 # Newline removed from strip_line is not "whitespace"
else
- @empty = false
- @indent = line.length - strip_line.length
+ line.length - strip_line.length
end
set_kw_end
diff --git a/lib/syntax_suggest/core_ext.rb b/lib/syntax_suggest/core_ext.rb
index aed93e129c..e0fd62b81c 100644
--- a/lib/syntax_suggest/core_ext.rb
+++ b/lib/syntax_suggest/core_ext.rb
@@ -45,6 +45,8 @@ if SyntaxError.method_defined?(:detailed_message)
)
annotation = io.string
+ annotation += "\n" unless annotation.end_with?("\n")
+
annotation + message
else
message
@@ -66,9 +68,13 @@ if SyntaxError.method_defined?(:detailed_message)
else
autoload :Pathname, "pathname"
+ #--
# Monkey patch kernel to ensure that all `require` calls call the same
# method
+ #++
module Kernel
+ # :stopdoc:
+
module_function
alias_method :syntax_suggest_original_require, :require
diff --git a/lib/syntax_suggest/parse_blocks_from_indent_line.rb b/lib/syntax_suggest/parse_blocks_from_indent_line.rb
index d1071732fe..241ed6acb4 100644
--- a/lib/syntax_suggest/parse_blocks_from_indent_line.rb
+++ b/lib/syntax_suggest/parse_blocks_from_indent_line.rb
@@ -36,8 +36,8 @@ module SyntaxSuggest
# Builds blocks from bottom up
def each_neighbor_block(target_line)
scan = AroundBlockScan.new(code_lines: code_lines, block: CodeBlock.new(lines: target_line))
- .skip(:empty?)
- .skip(:hidden?)
+ .force_add_empty
+ .force_add_hidden
.scan_while { |line| line.indent >= target_line.indent }
neighbors = scan.code_block.lines
diff --git a/lib/syntax_suggest/scan_history.rb b/lib/syntax_suggest/scan_history.rb
new file mode 100644
index 0000000000..d15597c440
--- /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/lib/syntax_suggest/syntax_suggest.gemspec b/lib/syntax_suggest/syntax_suggest.gemspec
index 73b25c6a5f..0e611c13d0 100644
--- a/lib/syntax_suggest/syntax_suggest.gemspec
+++ b/lib/syntax_suggest/syntax_suggest.gemspec
@@ -27,6 +27,6 @@ Gem::Specification.new do |spec|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|assets)/}) }
end
spec.bindir = "exe"
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
+ spec.executables = ["syntax_suggest"]
spec.require_paths = ["lib"]
end
diff --git a/lib/syntax_suggest/version.rb b/lib/syntax_suggest/version.rb
index d9ea5200e6..ac8c2f62e5 100644
--- a/lib/syntax_suggest/version.rb
+++ b/lib/syntax_suggest/version.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
module SyntaxSuggest
- VERSION = "1.0.2"
+ VERSION = "1.1.0"
end
diff --git a/spec/syntax_suggest/integration/exe_cli_spec.rb b/spec/syntax_suggest/integration/exe_cli_spec.rb
index f0b49b4386..b9a3173715 100644
--- a/spec/syntax_suggest/integration/exe_cli_spec.rb
+++ b/spec/syntax_suggest/integration/exe_cli_spec.rb
@@ -13,7 +13,8 @@ module SyntaxSuggest
end
def exe(cmd)
- out = run!("#{exe_path} #{cmd}", raise_on_nonzero_exit: false)
+ ruby = ENV.fetch("RUBY", "ruby")
+ out = run!("#{ruby} #{exe_path} #{cmd}", raise_on_nonzero_exit: false)
puts out if ENV["SYNTAX_SUGGEST_DEBUG"]
out
end
diff --git a/spec/syntax_suggest/integration/ruby_command_line_spec.rb b/spec/syntax_suggest/integration/ruby_command_line_spec.rb
index 61102dad2a..b41a4c86e3 100644
--- a/spec/syntax_suggest/integration/ruby_command_line_spec.rb
+++ b/spec/syntax_suggest/integration/ruby_command_line_spec.rb
@@ -46,6 +46,24 @@ module SyntaxSuggest
end
end
+ # Since Ruby 3.2 includes syntax_suggest as a default gem, we might accidentally
+ # be requiring the default gem instead of this library under test. Assert that's
+ # not the case
+ it "tests current version of syntax_suggest" do
+ Dir.mktmpdir do |dir|
+ tmpdir = Pathname(dir)
+ script = tmpdir.join("script.rb")
+ contents = <<~'EOM'
+ puts "suggest_version is #{SyntaxSuggest::VERSION}"
+ EOM
+ script.write(contents)
+
+ out = `#{ruby} -I#{lib_dir} -rsyntax_suggest/version #{script} 2>&1`
+
+ expect(out).to include("suggest_version is #{SyntaxSuggest::VERSION}").once
+ end
+ end
+
it "detects require error and adds a message with auto mode" do
Dir.mktmpdir do |dir|
tmpdir = Pathname(dir)
diff --git a/spec/syntax_suggest/integration/syntax_suggest_spec.rb b/spec/syntax_suggest/integration/syntax_suggest_spec.rb
index bb50fafce7..64dafabcdd 100644
--- a/spec/syntax_suggest/integration/syntax_suggest_spec.rb
+++ b/spec/syntax_suggest/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)
@@ -115,9 +116,6 @@ module SyntaxSuggest
expect(io.string).to include(<<~'EOM')
5 module DerailedBenchmarks
6 class RequireTree
- 7 REQUIRED_BY = {}
- 9 attr_reader :name
- 10 attr_writer :cost
> 13 def initialize(name)
> 18 def self.reset!
> 25 end
@@ -160,7 +158,6 @@ module SyntaxSuggest
out = io.string
expect(out).to include(<<~EOM)
16 class Rexe
- 18 VERSION = '1.5.1'
> 77 class Lookups
> 140 def format_requires
> 148 end
@@ -207,5 +204,36 @@ module SyntaxSuggest
> 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/syntax_suggest/spec_helper.rb b/spec/syntax_suggest/spec_helper.rb
index 67d401888b..89bc9f4ab1 100644
--- a/spec/syntax_suggest/spec_helper.rb
+++ b/spec/syntax_suggest/spec_helper.rb
@@ -16,6 +16,12 @@ RSpec.configure do |config|
config.expect_with :rspec do |c|
c.syntax = :expect
end
+
+ if config.color_mode == :automatic
+ if config.color_enabled? && ((ENV["TERM"] == "dumb") || ENV["NO_COLOR"]&.slice(0))
+ config.color_mode = :off
+ end
+ end
end
# Used for debugging modifications to
diff --git a/spec/syntax_suggest/unit/around_block_scan_spec.rb b/spec/syntax_suggest/unit/around_block_scan_spec.rb
index 6053c3947e..d6756448bd 100644
--- a/spec/syntax_suggest/unit/around_block_scan_spec.rb
+++ b/spec/syntax_suggest/unit/around_block_scan_spec.rb
@@ -13,7 +13,7 @@ module SyntaxSuggest
code_lines = CodeLine.from_source(source)
block = CodeBlock.new(lines: code_lines[1])
expand = AroundBlockScan.new(code_lines: code_lines, block: block)
- .scan_neighbors
+ .scan_neighbors_not_empty
expect(expand.code_block.to_s).to eq(source)
expand.scan_while { |line| false }
@@ -104,8 +104,8 @@ module SyntaxSuggest
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
@@ -149,9 +149,9 @@ module SyntaxSuggest
block = CodeBlock.new(lines: code_lines[3])
expand = AroundBlockScan.new(code_lines: code_lines, block: block)
- expand.skip(:empty?)
- expand.skip(:hidden?)
- expand.scan_neighbors
+ expand.force_add_empty
+ expand.force_add_hidden
+ expand.scan_neighbors_not_empty
expect(expand.code_block.to_s).to eq(<<~EOM.indent(4))
diff --git a/spec/syntax_suggest/unit/block_expand_spec.rb b/spec/syntax_suggest/unit/block_expand_spec.rb
index ba0b0457a1..5cff73621d 100644
--- a/spec/syntax_suggest/unit/block_expand_spec.rb
+++ b/spec/syntax_suggest/unit/block_expand_spec.rb
@@ -4,6 +4,36 @@ require_relative "../spec_helper"
module SyntaxSuggest
RSpec.describe BlockExpand do
+ it "empty line in methods" do
+ source_string = <<~EOM
+ class Dog # index 0
+ def bark # index 1
+
+ end # index 3
+
+ def sit # index 5
+ print "sit" # index 6
+ end # index 7
+ end # index 8
+ end # extra end
+ EOM
+
+ code_lines = code_line_array(source_string)
+
+ sit = code_lines[4..7]
+ sit.each(&:mark_invisible)
+
+ block = CodeBlock.new(lines: sit)
+ expansion = BlockExpand.new(code_lines: code_lines)
+ block = expansion.expand_neighbors(block)
+
+ expect(block.to_s).to eq(<<~EOM.indent(2))
+ def bark # index 1
+
+ end # index 3
+ EOM
+ end
+
it "captures multiple empty and hidden lines" do
source_string = <<~EOM
def foo
diff --git a/spec/syntax_suggest/unit/capture/before_after_keyword_ends_spec.rb b/spec/syntax_suggest/unit/capture/before_after_keyword_ends_spec.rb
new file mode 100644
index 0000000000..02d9be4387
--- /dev/null
+++ b/spec/syntax_suggest/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/syntax_suggest/unit/capture/falling_indent_lines_spec.rb b/spec/syntax_suggest/unit/capture/falling_indent_lines_spec.rb
new file mode 100644
index 0000000000..61d1642d97
--- /dev/null
+++ b/spec/syntax_suggest/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/syntax_suggest/unit/capture_code_context_spec.rb b/spec/syntax_suggest/unit/capture_code_context_spec.rb
index e1bc281c13..46f13e8961 100644
--- a/spec/syntax_suggest/unit/capture_code_context_spec.rb
+++ b/spec/syntax_suggest/unit/capture_code_context_spec.rb
@@ -4,6 +4,32 @@ require_relative "../spec_helper"
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
@@ -16,13 +42,14 @@ module SyntaxSuggest
EOM
code_lines = CleanDocument.new(source: source).call.lines
- block = CodeBlock.new(lines: code_lines[0])
+ block = CodeBlock.new(lines: code_lines[3])
display = CaptureCodeContext.new(
blocks: [block],
code_lines: code_lines
)
- lines = display.call
+
+ lines = display.capture_before_after_kws(block).sort
expect(lines.join).to eq(<<~'EOM')
def sit
end
diff --git a/spec/syntax_suggest/unit/clean_document_spec.rb b/spec/syntax_suggest/unit/clean_document_spec.rb
index 4fb79efd62..25a62e4454 100644
--- a/spec/syntax_suggest/unit/clean_document_spec.rb
+++ b/spec/syntax_suggest/unit/clean_document_spec.rb
@@ -72,6 +72,24 @@ module SyntaxSuggest
EOM
end
+ it "joins multi-line chained methods when separated by comments" do
+ source = <<~EOM
+ User.
+ # comment
+ where(name: 'schneems').
+ # another comment
+ first
+ EOM
+
+ doc = CleanDocument.new(source: source).join_consecutive!
+ code_lines = doc.lines
+
+ expect(code_lines[0].to_s.count($/)).to eq(5)
+ code_lines[1..-1].each do |line|
+ expect(line.to_s.strip.length).to eq(0)
+ end
+ end
+
it "helper method: take_while_including" do
source = <<~EOM
User
@@ -92,27 +110,10 @@ module SyntaxSuggest
# yolo
EOM
- out = CleanDocument.new(source: source).lines.join
- expect(out.to_s).to eq(<<~EOM)
-
- puts "what"
-
- EOM
- end
-
- it "whitespace: removes whitespace" do
- source = " \n" + <<~EOM
- puts "what"
- EOM
-
- out = CleanDocument.new(source: source).lines.join
- expect(out.to_s).to eq(<<~EOM)
-
- puts "what"
- EOM
-
- expect(source.lines.first.to_s).to_not eq("\n")
- expect(out.lines.first.to_s).to eq("\n")
+ lines = CleanDocument.new(source: source).lines
+ expect(lines[0].to_s).to eq($/)
+ expect(lines[1].to_s).to eq('puts "what"' + $/)
+ expect(lines[2].to_s).to eq($/)
end
it "trailing slash: does not join trailing do" do
diff --git a/spec/syntax_suggest/unit/code_line_spec.rb b/spec/syntax_suggest/unit/code_line_spec.rb
index cc4fa48bc9..d5b568fd19 100644
--- a/spec/syntax_suggest/unit/code_line_spec.rb
+++ b/spec/syntax_suggest/unit/code_line_spec.rb
@@ -48,6 +48,7 @@ module SyntaxSuggest
# Indicates line 1 can join 2, 2 can join 3, but 3 won't join it's next line
expect(code_lines.map(&:ignore_newline_not_beg?)).to eq([true, true, false, false])
end
+
it "trailing if" do
code_lines = CodeLine.from_source(<<~'EOM')
puts "lol" if foo
diff --git a/spec/syntax_suggest/unit/core_ext_spec.rb b/spec/syntax_suggest/unit/core_ext_spec.rb
new file mode 100644
index 0000000000..802d03ecc0
--- /dev/null
+++ b/spec/syntax_suggest/unit/core_ext_spec.rb
@@ -0,0 +1,34 @@
+require_relative "../spec_helper"
+
+module SyntaxSuggest
+ RSpec.describe "Core extension" do
+ it "SyntaxError monkepatch ensures there is a newline to the end of the file" do
+ skip if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.2")
+
+ Dir.mktmpdir do |dir|
+ tmpdir = Pathname(dir)
+ file = tmpdir.join("file.rb")
+ file.write(<<~'EOM'.strip)
+ print 'no newline
+ EOM
+
+ core_ext_file = lib_dir.join("syntax_suggest").join("core_ext")
+ require_relative core_ext_file
+
+ original_message = "blerg"
+ error = SyntaxError.new(original_message)
+ def error.set_tmp_path_for_testing=(path)
+ @tmp_path_for_testing = path
+ end
+ error.set_tmp_path_for_testing = file
+ def error.path
+ @tmp_path_for_testing
+ end
+
+ detailed = error.detailed_message(highlight: false, syntax_suggest: true)
+ expect(detailed).to include("'no newline\n#{original_message}")
+ expect(detailed).to_not include("print 'no newline#{original_message}")
+ end
+ end
+ end
+end
diff --git a/spec/syntax_suggest/unit/display_invalid_blocks_spec.rb b/spec/syntax_suggest/unit/display_invalid_blocks_spec.rb
index 2c59d9dc78..b11d7d242e 100644
--- a/spec/syntax_suggest/unit/display_invalid_blocks_spec.rb
+++ b/spec/syntax_suggest/unit/display_invalid_blocks_spec.rb
@@ -144,6 +144,7 @@ module SyntaxSuggest
expect(io.string).to include([
" 1 class OH",
"> 2 def hello",
+ " 3 def hai",
" 4 end",
" 5 end",
""
@@ -162,6 +163,7 @@ module SyntaxSuggest
[
" 1 class OH",
["> 2 ", DisplayCodeWithLineNumbers::TERMINAL_HIGHLIGHT, " def hello"].join,
+ " 3 def hai",
" 4 end",
" 5 end",
""
diff --git a/spec/syntax_suggest/unit/scan_history_spec.rb b/spec/syntax_suggest/unit/scan_history_spec.rb
new file mode 100644
index 0000000000..0e75ac66ce
--- /dev/null
+++ b/spec/syntax_suggest/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