summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/syntax_suggest/around_block_scan.rb51
-rw-r--r--lib/syntax_suggest/block_expand.rb97
-rw-r--r--lib/syntax_suggest/capture_code_context.rb1
-rw-r--r--spec/syntax_suggest/integration/syntax_suggest_spec.rb25
-rw-r--r--spec/syntax_suggest/unit/around_block_scan_spec.rb4
-rw-r--r--spec/syntax_suggest/unit/block_expand_spec.rb30
-rw-r--r--spec/syntax_suggest/unit/code_search_spec.rb1
7 files changed, 197 insertions, 12 deletions
diff --git a/lib/syntax_suggest/around_block_scan.rb b/lib/syntax_suggest/around_block_scan.rb
index 2a57d1b19e..4793c3b5e6 100644
--- a/lib/syntax_suggest/around_block_scan.rb
+++ b/lib/syntax_suggest/around_block_scan.rb
@@ -61,7 +61,6 @@ module SyntaxSuggest
def scan_while
stop_next = false
-
kw_count = 0
end_count = 0
index = before_lines.reverse_each.take_while do |line|
@@ -166,7 +165,55 @@ module SyntaxSuggest
end
end
- def scan_neighbors
+ # 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
+ 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
+
+ def scan_neighbors_not_empty
scan_while { |line| line.not_empty? && line.indent >= @orig_indent }
end
diff --git a/lib/syntax_suggest/block_expand.rb b/lib/syntax_suggest/block_expand.rb
index 396b2c3a1a..8142e74869 100644
--- a/lib/syntax_suggest/block_expand.rb
+++ b/lib/syntax_suggest/block_expand.rb
@@ -35,14 +35,31 @@ 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?)
@@ -51,14 +68,82 @@ module SyntaxSuggest
.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)
+ neighbors = AroundBlockScan.new(code_lines: @code_lines, block: block)
.skip(:hidden?)
.stop_after_kw
- .scan_neighbors
- .scan_while { |line| line.empty? } # Slurp up empties
+ .scan_neighbors_not_empty
+
+ # Slurp up empties
+ with_empties = neighbors
+ .scan_while { |line| line.empty? }
+
+ # If next line is kw and it will balance us, take it
+ expanded_lines = with_empties
+ .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_code_context.rb b/lib/syntax_suggest/capture_code_context.rb
index 7d6a550612..547072e2bd 100644
--- a/lib/syntax_suggest/capture_code_context.rb
+++ b/lib/syntax_suggest/capture_code_context.rb
@@ -76,7 +76,6 @@ module SyntaxSuggest
# end
# end
#
- #
def capture_falling_indent(block)
AroundBlockScan.new(
block: block,
diff --git a/spec/syntax_suggest/integration/syntax_suggest_spec.rb b/spec/syntax_suggest/integration/syntax_suggest_spec.rb
index 21c02ca287..e96173717d 100644
--- a/spec/syntax_suggest/integration/syntax_suggest_spec.rb
+++ b/spec/syntax_suggest/integration/syntax_suggest_spec.rb
@@ -234,5 +234,30 @@ module SyntaxSuggest
> 10 end # extra end
EOM
end
+
+ it "space inside of a method" do
+ source = <<~'EOM'
+ class Dog # 1
+ def bark # 2
+
+ end # 4
+
+ def sit # 6
+ print "sit" # 7
+ end # 8
+ end # 9
+ end # extra end
+ EOM
+
+ io = StringIO.new
+ SyntaxSuggest.call(
+ io: io,
+ source: source
+ )
+ out = io.string
+ expect(out).to include(<<~EOM)
+ > 10 end # extra end
+ EOM
+ end
end
end
diff --git a/spec/syntax_suggest/unit/around_block_scan_spec.rb b/spec/syntax_suggest/unit/around_block_scan_spec.rb
index 6053c3947e..be1c3a4780 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 }
@@ -151,7 +151,7 @@ module SyntaxSuggest
expand = AroundBlockScan.new(code_lines: code_lines, block: block)
expand.skip(:empty?)
expand.skip(:hidden?)
- expand.scan_neighbors
+ 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..4f93210368 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/code_search_spec.rb b/spec/syntax_suggest/unit/code_search_spec.rb
index 9a8115c0c8..f836ba36f3 100644
--- a/spec/syntax_suggest/unit/code_search_spec.rb
+++ b/spec/syntax_suggest/unit/code_search_spec.rb
@@ -338,7 +338,6 @@ module SyntaxSuggest
end
EOM
search.call
- puts "done"
expect(search.invalid_blocks.join).to eq(<<~'EOM')
Foo.call do