Bump up syntax_suggest-1.1.0

This commit is contained in:
Hiroshi SHIBATA 2023-06-01 17:05:35 +09:00 committed by nagachika
parent 0c908fa681
commit e517ba2e5b
27 changed files with 967 additions and 205 deletions

View file

@ -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

View file

@ -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
@force_add_empty = false
@force_add_hidden = false
@target_indent = nil
@skip_hidden = false
@skip_empty = false
@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
stop_next_up = true
end
yield line
end.last&.index
},
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
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?
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
stop_next_down = 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
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
when -1
if next_down&.is_end? && next_down.indent >= @target_indent
@scanner.scan(
up: ->(line, _, _) { false },
down: ->(line, _, _) { line == next_down }
)
@scanner.commit_if_changed
end
end
# Rollback any uncommitted changes
@scanner.stash_changes
self
end
lines.reverse!
kw_count = 0
end_count = 0
after_lines.each do |line|
next if line.empty?
break if line.indent < @orig_indent
next if line.indent != @orig_indent
kw_count += 1 if line.is_kw?
end_count += 1 if line.is_end?
if kw_count != 0 && kw_count == end_count
lines << line
break
end
lines << line
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
end
end
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

View file

@ -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
end
next_block
else
expand_indent(block)
end
def expand_indent(block)
AroundBlockScan.new(code_lines: @code_lines, block: block)
.skip(:hidden?)
.stop_after_kw
.scan_adjacent_indent
.code_block
end
def expand_neighbors(block)
expanded_lines = AroundBlockScan.new(code_lines: @code_lines, block: block)
.skip(:hidden?)
# 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)
now = AroundBlockScan.new(code_lines: @code_lines, block: block)
.force_add_hidden
.stop_after_kw
.scan_neighbors
.scan_while { |line| line.empty? } # Slurp up empties
.scan_adjacent_indent
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)
now = AroundBlockScan.new(code_lines: @code_lines, block: block)
# Initial scan
now
.force_add_hidden
.stop_after_kw
.scan_neighbors_not_empty
# Slurp up empties
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

View 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

View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View file

@ -1,5 +1,5 @@
# frozen_string_literal: true
module SyntaxSuggest
VERSION = "1.0.2"
VERSION = "1.1.0"
end

View file

@ -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

View file

@ -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)

View file

@ -21,9 +21,10 @@ module SyntaxSuggest
filename: file
)
end
end
debug_display(io.string)
debug_display(benchmark)
end
expect(io.string).to include(<<~'EOM')
6 class SyntaxTree < Ripper
@ -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

View file

@ -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

View file

@ -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))

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View file

@ -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",
""

View 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