mirror of
https://github.com/ruby/ruby.git
synced 2025-09-17 01:23:57 +02:00
Bump up syntax_suggest-1.1.0
This commit is contained in:
parent
0c908fa681
commit
e517ba2e5b
27 changed files with 967 additions and 205 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
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#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
|
||||
|
||||
# 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
|
||||
stop_next_up = false
|
||||
stop_next_down = 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?
|
||||
@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?
|
||||
|
||||
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
|
||||
if @stop_after_kw && kw_count > end_count
|
||||
stop_next_up = true
|
||||
end
|
||||
|
||||
yield line
|
||||
end.last&.index
|
||||
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?
|
||||
|
||||
if index && index < before_index
|
||||
@before_index = index
|
||||
end
|
||||
if @stop_after_kw && end_count > kw_count
|
||||
stop_next_down = 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
|
||||
}
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
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!
|
||||
return self if kw_count == end_count # nothing to balance
|
||||
|
||||
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
|
||||
@scanner.commit_if_changed # Rollback point if we don't find anything to optimize
|
||||
|
||||
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
|
||||
# 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
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
def scan_neighbors
|
||||
scan_while { |line| line.not_empty? && line.indent >= @orig_indent }
|
||||
end
|
||||
|
||||
def next_up
|
||||
@code_lines[before_index.pred]
|
||||
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)
|
||||
|
||||
@target_indent = before_after_indent.min
|
||||
scan_while { |line| line.not_empty? && line.indent >= @target_indent }
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
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]
|
||||
@scanner.lines
|
||||
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] || []
|
||||
end
|
||||
|
||||
private def after_lines
|
||||
@code_lines[after_index.next..-1] || []
|
||||
# Managable rspec errors
|
||||
def inspect
|
||||
"#<#{self.class}:0x0000123843lol >"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
85
lib/syntax_suggest/capture/before_after_keyword_ends.rb
Normal file
85
lib/syntax_suggest/capture/before_after_keyword_ends.rb
Normal file
|
@ -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
|
71
lib/syntax_suggest/capture/falling_indent_lines.rb
Normal file
71
lib/syntax_suggest/capture/falling_indent_lines.rb
Normal file
|
@ -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
|
|
@ -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 @@ 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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
134
lib/syntax_suggest/scan_history.rb
Normal file
134
lib/syntax_suggest/scan_history.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SyntaxSuggest
|
||||
VERSION = "1.0.2"
|
||||
VERSION = "1.1.0"
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
34
spec/syntax_suggest/unit/core_ext_spec.rb
Normal file
34
spec/syntax_suggest/unit/core_ext_spec.rb
Normal file
|
@ -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
|
|
@ -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",
|
||||
""
|
||||
|
|
114
spec/syntax_suggest/unit/scan_history_spec.rb
Normal file
114
spec/syntax_suggest/unit/scan_history_spec.rb
Normal file
|
@ -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
|
Loading…
Add table
Add a link
Reference in a new issue