mirror of
https://github.com/ruby/ruby.git
synced 2025-09-17 09:33:59 +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
|
code_lines: search.code_lines
|
||||||
).call
|
).call
|
||||||
rescue Timeout::Error => e
|
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($/)
|
io.puts e.backtrace.first(3).join($/)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -91,7 +91,9 @@ module SyntaxSuggest
|
||||||
dir = Pathname(dir)
|
dir = Pathname(dir)
|
||||||
dir.join(time).tap { |path|
|
dir.join(time).tap { |path|
|
||||||
path.mkpath
|
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
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require_relative "scan_history"
|
||||||
|
|
||||||
module SyntaxSuggest
|
module SyntaxSuggest
|
||||||
# This class is useful for exploring contents before and after
|
# This class is useful for exploring contents before and after
|
||||||
# a block
|
# a block
|
||||||
|
@ -24,201 +26,207 @@ module SyntaxSuggest
|
||||||
# puts scan.before_index # => 0
|
# puts scan.before_index # => 0
|
||||||
# puts scan.after_index # => 3
|
# 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
|
class AroundBlockScan
|
||||||
def initialize(code_lines:, block:)
|
def initialize(code_lines:, block:)
|
||||||
@code_lines = code_lines
|
@code_lines = code_lines
|
||||||
@orig_before_index = block.lines.first.index
|
|
||||||
@orig_after_index = block.lines.last.index
|
|
||||||
@orig_indent = block.current_indent
|
@orig_indent = block.current_indent
|
||||||
@skip_array = []
|
|
||||||
@after_array = []
|
|
||||||
@before_array = []
|
|
||||||
@stop_after_kw = false
|
|
||||||
|
|
||||||
@skip_hidden = false
|
@stop_after_kw = false
|
||||||
@skip_empty = false
|
@force_add_empty = false
|
||||||
|
@force_add_hidden = false
|
||||||
|
@target_indent = nil
|
||||||
|
|
||||||
|
@scanner = ScanHistory.new(code_lines: code_lines, block: block)
|
||||||
end
|
end
|
||||||
|
|
||||||
def skip(name)
|
# When using this flag, `scan_while` will
|
||||||
case name
|
# bypass the block it's given and always add a
|
||||||
when :hidden?
|
# line that responds truthy to `CodeLine#hidden?`
|
||||||
@skip_hidden = true
|
#
|
||||||
when :empty?
|
# Lines are hidden when they've been evaluated by
|
||||||
@skip_empty = true
|
# the parser as part of a block and found to contain
|
||||||
else
|
# valid code.
|
||||||
raise "Unsupported skip #{name}"
|
def force_add_hidden
|
||||||
end
|
@force_add_hidden = true
|
||||||
self
|
self
|
||||||
end
|
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
|
def stop_after_kw
|
||||||
@stop_after_kw = true
|
@stop_after_kw = true
|
||||||
self
|
self
|
||||||
end
|
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
|
def scan_while
|
||||||
stop_next = false
|
stop_next_up = false
|
||||||
|
stop_next_down = false
|
||||||
|
|
||||||
kw_count = 0
|
@scanner.scan(
|
||||||
end_count = 0
|
up: ->(line, kw_count, end_count) {
|
||||||
index = before_lines.reverse_each.take_while do |line|
|
next false if stop_next_up
|
||||||
next false if stop_next
|
next true if @force_add_hidden && line.hidden?
|
||||||
next true if @skip_hidden && line.hidden?
|
next true if @force_add_empty && line.empty?
|
||||||
next true if @skip_empty && line.empty?
|
|
||||||
|
|
||||||
kw_count += 1 if line.is_kw?
|
if @stop_after_kw && kw_count > end_count
|
||||||
end_count += 1 if line.is_end?
|
stop_next_up = true
|
||||||
if @stop_after_kw && kw_count > end_count
|
end
|
||||||
stop_next = true
|
|
||||||
end
|
|
||||||
|
|
||||||
yield line
|
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
|
if @stop_after_kw && end_count > kw_count
|
||||||
@before_index = index
|
stop_next_down = true
|
||||||
end
|
end
|
||||||
|
|
||||||
stop_next = false
|
yield line
|
||||||
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
|
|
||||||
end
|
|
||||||
|
|
||||||
yield line
|
|
||||||
end.last&.index
|
|
||||||
|
|
||||||
if index && index > after_index
|
|
||||||
@after_index = index
|
|
||||||
end
|
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
def capture_neighbor_context
|
# Scanning is intentionally conservative because
|
||||||
lines = []
|
# 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
|
kw_count = 0
|
||||||
end_count = 0
|
end_count = 0
|
||||||
before_lines.reverse_each do |line|
|
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?
|
kw_count += 1 if line.is_kw?
|
||||||
end_count += 1 if line.is_end?
|
end_count += 1 if line.is_end?
|
||||||
if kw_count != 0 && kw_count == end_count
|
|
||||||
lines << line
|
|
||||||
break
|
|
||||||
end
|
|
||||||
|
|
||||||
lines << line
|
|
||||||
end
|
end
|
||||||
|
|
||||||
lines.reverse!
|
return self if kw_count == end_count # nothing to balance
|
||||||
|
|
||||||
kw_count = 0
|
@scanner.commit_if_changed # Rollback point if we don't find anything to optimize
|
||||||
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?
|
# Try to eat up empty lines
|
||||||
end_count += 1 if line.is_end?
|
@scanner.scan(
|
||||||
if kw_count != 0 && kw_count == end_count
|
up: ->(line, _, _) { line.hidden? || line.empty? },
|
||||||
lines << line
|
down: ->(line, _, _) { line.hidden? || line.empty? }
|
||||||
break
|
)
|
||||||
|
|
||||||
|
# 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
|
end
|
||||||
|
when -1
|
||||||
lines << line
|
if next_down&.is_end? && next_down.indent >= @target_indent
|
||||||
end
|
@scanner.scan(
|
||||||
|
up: ->(line, _, _) { false },
|
||||||
lines
|
down: ->(line, _, _) { line == next_down }
|
||||||
end
|
)
|
||||||
|
@scanner.commit_if_changed
|
||||||
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
|
||||||
end
|
end
|
||||||
|
# Rollback any uncommitted changes
|
||||||
|
@scanner.stash_changes
|
||||||
|
|
||||||
last_indent = @orig_indent
|
self
|
||||||
after_lines.each do |line|
|
|
||||||
next if line.empty?
|
|
||||||
if line.indent < last_indent
|
|
||||||
yield line
|
|
||||||
last_indent = line.indent
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def scan_neighbors
|
# Finds code lines at the same or greater indentation and adds them
|
||||||
scan_while { |line| line.not_empty? && line.indent >= @orig_indent }
|
# to the block
|
||||||
end
|
def scan_neighbors_not_empty
|
||||||
|
@target_indent = @orig_indent
|
||||||
def next_up
|
scan_while { |line| line.not_empty? && line.indent >= @target_indent }
|
||||||
@code_lines[before_index.pred]
|
|
||||||
end
|
|
||||||
|
|
||||||
def next_down
|
|
||||||
@code_lines[after_index.next]
|
|
||||||
end
|
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
|
def scan_adjacent_indent
|
||||||
before_after_indent = []
|
before_after_indent = []
|
||||||
before_after_indent << (next_up&.indent || 0)
|
|
||||||
before_after_indent << (next_down&.indent || 0)
|
|
||||||
|
|
||||||
indent = before_after_indent.min
|
before_after_indent << (@scanner.next_up&.indent || 0)
|
||||||
scan_while { |line| line.not_empty? && line.indent >= indent }
|
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
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
def start_at_next_line
|
# Return the currently matched lines as a `CodeBlock`
|
||||||
before_index
|
#
|
||||||
after_index
|
# When a `CodeBlock` is created it will gather metadata about
|
||||||
@before_index -= 1
|
# itself, so this is not a free conversion. Avoid allocating
|
||||||
@after_index += 1
|
# more CodeBlock's than needed
|
||||||
self
|
|
||||||
end
|
|
||||||
|
|
||||||
def code_block
|
def code_block
|
||||||
CodeBlock.new(lines: lines)
|
CodeBlock.new(lines: lines)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Returns the lines matched by the current scan as an
|
||||||
|
# array of CodeLines
|
||||||
def lines
|
def lines
|
||||||
@code_lines[before_index..after_index]
|
@scanner.lines
|
||||||
end
|
end
|
||||||
|
|
||||||
def before_index
|
# Managable rspec errors
|
||||||
@before_index ||= @orig_before_index
|
def inspect
|
||||||
end
|
"#<#{self.class}:0x0000123843lol >"
|
||||||
|
|
||||||
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] || []
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -35,30 +35,121 @@ module SyntaxSuggest
|
||||||
@code_lines = code_lines
|
@code_lines = code_lines
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Main interface. Expand current indentation, before
|
||||||
|
# expanding to a lower indentation
|
||||||
def call(block)
|
def call(block)
|
||||||
if (next_block = expand_neighbors(block))
|
if (next_block = expand_neighbors(block))
|
||||||
return next_block
|
next_block
|
||||||
|
else
|
||||||
|
expand_indent(block)
|
||||||
end
|
end
|
||||||
|
|
||||||
expand_indent(block)
|
|
||||||
end
|
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)
|
def expand_indent(block)
|
||||||
AroundBlockScan.new(code_lines: @code_lines, block: block)
|
now = AroundBlockScan.new(code_lines: @code_lines, block: block)
|
||||||
.skip(:hidden?)
|
.force_add_hidden
|
||||||
.stop_after_kw
|
.stop_after_kw
|
||||||
.scan_adjacent_indent
|
.scan_adjacent_indent
|
||||||
.code_block
|
|
||||||
|
now.lookahead_balance_one_line
|
||||||
|
|
||||||
|
now.code_block
|
||||||
end
|
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)
|
def expand_neighbors(block)
|
||||||
expanded_lines = AroundBlockScan.new(code_lines: @code_lines, block: block)
|
now = AroundBlockScan.new(code_lines: @code_lines, block: block)
|
||||||
.skip(:hidden?)
|
|
||||||
|
# Initial scan
|
||||||
|
now
|
||||||
|
.force_add_hidden
|
||||||
.stop_after_kw
|
.stop_after_kw
|
||||||
.scan_neighbors
|
.scan_neighbors_not_empty
|
||||||
.scan_while { |line| line.empty? } # Slurp up empties
|
|
||||||
|
# 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
|
.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
|
if block.lines == expanded_lines
|
||||||
nil
|
nil
|
||||||
else
|
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
|
# 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
|
module SyntaxSuggest
|
||||||
# Turns a "invalid block(s)" into useful context
|
# Turns a "invalid block(s)" into useful context
|
||||||
#
|
#
|
||||||
|
@ -55,6 +63,10 @@ module SyntaxSuggest
|
||||||
capture_falling_indent(block)
|
capture_falling_indent(block)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
sorted_lines
|
||||||
|
end
|
||||||
|
|
||||||
|
def sorted_lines
|
||||||
@lines_to_output.select!(&:not_empty?)
|
@lines_to_output.select!(&:not_empty?)
|
||||||
@lines_to_output.uniq!
|
@lines_to_output.uniq!
|
||||||
@lines_to_output.sort!
|
@lines_to_output.sort!
|
||||||
|
@ -76,12 +88,11 @@ module SyntaxSuggest
|
||||||
# end
|
# end
|
||||||
# end
|
# end
|
||||||
#
|
#
|
||||||
#
|
|
||||||
def capture_falling_indent(block)
|
def capture_falling_indent(block)
|
||||||
AroundBlockScan.new(
|
Capture::FallingIndentLines.new(
|
||||||
block: block,
|
block: block,
|
||||||
code_lines: @code_lines
|
code_lines: @code_lines
|
||||||
).on_falling_indent do |line|
|
).call do |line|
|
||||||
@lines_to_output << line
|
@lines_to_output << line
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -116,9 +127,10 @@ module SyntaxSuggest
|
||||||
def capture_before_after_kws(block)
|
def capture_before_after_kws(block)
|
||||||
return unless block.visible_lines.count == 1
|
return unless block.visible_lines.count == 1
|
||||||
|
|
||||||
around_lines = AroundBlockScan.new(code_lines: @code_lines, block: block)
|
around_lines = Capture::BeforeAfterKeywordEnds.new(
|
||||||
.start_at_next_line
|
code_lines: @code_lines,
|
||||||
.capture_neighbor_context
|
block: block
|
||||||
|
).call
|
||||||
|
|
||||||
around_lines -= block.lines
|
around_lines -= block.lines
|
||||||
|
|
||||||
|
|
|
@ -110,7 +110,7 @@ module SyntaxSuggest
|
||||||
@document.join
|
@document.join
|
||||||
end
|
end
|
||||||
|
|
||||||
# Remove comments and whitespace only lines
|
# Remove comments
|
||||||
#
|
#
|
||||||
# replace with empty newlines
|
# replace with empty newlines
|
||||||
#
|
#
|
||||||
|
@ -155,8 +155,10 @@ module SyntaxSuggest
|
||||||
# ).to eq(2)
|
# ).to eq(2)
|
||||||
#
|
#
|
||||||
def clean_sweep(source:)
|
def clean_sweep(source:)
|
||||||
|
# Match comments, but not HEREDOC strings with #{variable} interpolation
|
||||||
|
# https://rubular.com/r/HPwtW9OYxKUHXQ
|
||||||
source.lines.map do |line|
|
source.lines.map do |line|
|
||||||
if line.match?(/^\s*(#[^{].*)?$/) # https://rubular.com/r/LLE10D8HKMkJvs
|
if line.match?(/^\s*#([^{].*|)$/)
|
||||||
$/
|
$/
|
||||||
else
|
else
|
||||||
line
|
line
|
||||||
|
|
|
@ -48,12 +48,10 @@ module SyntaxSuggest
|
||||||
strip_line = line.dup
|
strip_line = line.dup
|
||||||
strip_line.lstrip!
|
strip_line.lstrip!
|
||||||
|
|
||||||
if strip_line.empty?
|
@indent = if (@empty = strip_line.empty?)
|
||||||
@empty = true
|
line.length - 1 # Newline removed from strip_line is not "whitespace"
|
||||||
@indent = 0
|
|
||||||
else
|
else
|
||||||
@empty = false
|
line.length - strip_line.length
|
||||||
@indent = line.length - strip_line.length
|
|
||||||
end
|
end
|
||||||
|
|
||||||
set_kw_end
|
set_kw_end
|
||||||
|
|
|
@ -45,6 +45,8 @@ if SyntaxError.method_defined?(:detailed_message)
|
||||||
)
|
)
|
||||||
annotation = io.string
|
annotation = io.string
|
||||||
|
|
||||||
|
annotation += "\n" unless annotation.end_with?("\n")
|
||||||
|
|
||||||
annotation + message
|
annotation + message
|
||||||
else
|
else
|
||||||
message
|
message
|
||||||
|
@ -66,9 +68,13 @@ if SyntaxError.method_defined?(:detailed_message)
|
||||||
else
|
else
|
||||||
autoload :Pathname, "pathname"
|
autoload :Pathname, "pathname"
|
||||||
|
|
||||||
|
#--
|
||||||
# Monkey patch kernel to ensure that all `require` calls call the same
|
# Monkey patch kernel to ensure that all `require` calls call the same
|
||||||
# method
|
# method
|
||||||
|
#++
|
||||||
module Kernel
|
module Kernel
|
||||||
|
# :stopdoc:
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
alias_method :syntax_suggest_original_require, :require
|
alias_method :syntax_suggest_original_require, :require
|
||||||
|
|
|
@ -36,8 +36,8 @@ module SyntaxSuggest
|
||||||
# Builds blocks from bottom up
|
# Builds blocks from bottom up
|
||||||
def each_neighbor_block(target_line)
|
def each_neighbor_block(target_line)
|
||||||
scan = AroundBlockScan.new(code_lines: code_lines, block: CodeBlock.new(lines: target_line))
|
scan = AroundBlockScan.new(code_lines: code_lines, block: CodeBlock.new(lines: target_line))
|
||||||
.skip(:empty?)
|
.force_add_empty
|
||||||
.skip(:hidden?)
|
.force_add_hidden
|
||||||
.scan_while { |line| line.indent >= target_line.indent }
|
.scan_while { |line| line.indent >= target_line.indent }
|
||||||
|
|
||||||
neighbors = scan.code_block.lines
|
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)/}) }
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|assets)/}) }
|
||||||
end
|
end
|
||||||
spec.bindir = "exe"
|
spec.bindir = "exe"
|
||||||
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
spec.executables = ["syntax_suggest"]
|
||||||
spec.require_paths = ["lib"]
|
spec.require_paths = ["lib"]
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module SyntaxSuggest
|
module SyntaxSuggest
|
||||||
VERSION = "1.0.2"
|
VERSION = "1.1.0"
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,7 +13,8 @@ module SyntaxSuggest
|
||||||
end
|
end
|
||||||
|
|
||||||
def exe(cmd)
|
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"]
|
puts out if ENV["SYNTAX_SUGGEST_DEBUG"]
|
||||||
out
|
out
|
||||||
end
|
end
|
||||||
|
|
|
@ -46,6 +46,24 @@ module SyntaxSuggest
|
||||||
end
|
end
|
||||||
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
|
it "detects require error and adds a message with auto mode" do
|
||||||
Dir.mktmpdir do |dir|
|
Dir.mktmpdir do |dir|
|
||||||
tmpdir = Pathname(dir)
|
tmpdir = Pathname(dir)
|
||||||
|
|
|
@ -21,10 +21,11 @@ module SyntaxSuggest
|
||||||
filename: file
|
filename: file
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
debug_display(io.string)
|
|
||||||
debug_display(benchmark)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
debug_display(io.string)
|
||||||
|
debug_display(benchmark)
|
||||||
|
|
||||||
expect(io.string).to include(<<~'EOM')
|
expect(io.string).to include(<<~'EOM')
|
||||||
6 class SyntaxTree < Ripper
|
6 class SyntaxTree < Ripper
|
||||||
170 def self.parse(source)
|
170 def self.parse(source)
|
||||||
|
@ -115,9 +116,6 @@ module SyntaxSuggest
|
||||||
expect(io.string).to include(<<~'EOM')
|
expect(io.string).to include(<<~'EOM')
|
||||||
5 module DerailedBenchmarks
|
5 module DerailedBenchmarks
|
||||||
6 class RequireTree
|
6 class RequireTree
|
||||||
7 REQUIRED_BY = {}
|
|
||||||
9 attr_reader :name
|
|
||||||
10 attr_writer :cost
|
|
||||||
> 13 def initialize(name)
|
> 13 def initialize(name)
|
||||||
> 18 def self.reset!
|
> 18 def self.reset!
|
||||||
> 25 end
|
> 25 end
|
||||||
|
@ -160,7 +158,6 @@ module SyntaxSuggest
|
||||||
out = io.string
|
out = io.string
|
||||||
expect(out).to include(<<~EOM)
|
expect(out).to include(<<~EOM)
|
||||||
16 class Rexe
|
16 class Rexe
|
||||||
18 VERSION = '1.5.1'
|
|
||||||
> 77 class Lookups
|
> 77 class Lookups
|
||||||
> 140 def format_requires
|
> 140 def format_requires
|
||||||
> 148 end
|
> 148 end
|
||||||
|
@ -207,5 +204,36 @@ module SyntaxSuggest
|
||||||
> 4 end
|
> 4 end
|
||||||
EOM
|
EOM
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,6 +16,12 @@ RSpec.configure do |config|
|
||||||
config.expect_with :rspec do |c|
|
config.expect_with :rspec do |c|
|
||||||
c.syntax = :expect
|
c.syntax = :expect
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
# Used for debugging modifications to
|
# Used for debugging modifications to
|
||||||
|
|
|
@ -13,7 +13,7 @@ module SyntaxSuggest
|
||||||
code_lines = CodeLine.from_source(source)
|
code_lines = CodeLine.from_source(source)
|
||||||
block = CodeBlock.new(lines: code_lines[1])
|
block = CodeBlock.new(lines: code_lines[1])
|
||||||
expand = AroundBlockScan.new(code_lines: code_lines, block: block)
|
expand = AroundBlockScan.new(code_lines: code_lines, block: block)
|
||||||
.scan_neighbors
|
.scan_neighbors_not_empty
|
||||||
|
|
||||||
expect(expand.code_block.to_s).to eq(source)
|
expect(expand.code_block.to_s).to eq(source)
|
||||||
expand.scan_while { |line| false }
|
expand.scan_while { |line| false }
|
||||||
|
@ -104,8 +104,8 @@ module SyntaxSuggest
|
||||||
expand = AroundBlockScan.new(code_lines: code_lines, block: block)
|
expand = AroundBlockScan.new(code_lines: code_lines, block: block)
|
||||||
expand.scan_while { true }
|
expand.scan_while { true }
|
||||||
|
|
||||||
expect(expand.before_index).to eq(0)
|
expect(expand.lines.first.index).to eq(0)
|
||||||
expect(expand.after_index).to eq(6)
|
expect(expand.lines.last.index).to eq(6)
|
||||||
expect(expand.code_block.to_s).to eq(source_string)
|
expect(expand.code_block.to_s).to eq(source_string)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -149,9 +149,9 @@ module SyntaxSuggest
|
||||||
|
|
||||||
block = CodeBlock.new(lines: code_lines[3])
|
block = CodeBlock.new(lines: code_lines[3])
|
||||||
expand = AroundBlockScan.new(code_lines: code_lines, block: block)
|
expand = AroundBlockScan.new(code_lines: code_lines, block: block)
|
||||||
expand.skip(:empty?)
|
expand.force_add_empty
|
||||||
expand.skip(:hidden?)
|
expand.force_add_hidden
|
||||||
expand.scan_neighbors
|
expand.scan_neighbors_not_empty
|
||||||
|
|
||||||
expect(expand.code_block.to_s).to eq(<<~EOM.indent(4))
|
expect(expand.code_block.to_s).to eq(<<~EOM.indent(4))
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,36 @@ require_relative "../spec_helper"
|
||||||
|
|
||||||
module SyntaxSuggest
|
module SyntaxSuggest
|
||||||
RSpec.describe BlockExpand do
|
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
|
it "captures multiple empty and hidden lines" do
|
||||||
source_string = <<~EOM
|
source_string = <<~EOM
|
||||||
def foo
|
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
|
module SyntaxSuggest
|
||||||
RSpec.describe CaptureCodeContext do
|
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
|
it "capture_before_after_kws" do
|
||||||
source = <<~'EOM'
|
source = <<~'EOM'
|
||||||
def sit
|
def sit
|
||||||
|
@ -16,13 +42,14 @@ module SyntaxSuggest
|
||||||
EOM
|
EOM
|
||||||
|
|
||||||
code_lines = CleanDocument.new(source: source).call.lines
|
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(
|
display = CaptureCodeContext.new(
|
||||||
blocks: [block],
|
blocks: [block],
|
||||||
code_lines: code_lines
|
code_lines: code_lines
|
||||||
)
|
)
|
||||||
lines = display.call
|
|
||||||
|
lines = display.capture_before_after_kws(block).sort
|
||||||
expect(lines.join).to eq(<<~'EOM')
|
expect(lines.join).to eq(<<~'EOM')
|
||||||
def sit
|
def sit
|
||||||
end
|
end
|
||||||
|
|
|
@ -72,6 +72,24 @@ module SyntaxSuggest
|
||||||
EOM
|
EOM
|
||||||
end
|
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
|
it "helper method: take_while_including" do
|
||||||
source = <<~EOM
|
source = <<~EOM
|
||||||
User
|
User
|
||||||
|
@ -92,27 +110,10 @@ module SyntaxSuggest
|
||||||
# yolo
|
# yolo
|
||||||
EOM
|
EOM
|
||||||
|
|
||||||
out = CleanDocument.new(source: source).lines.join
|
lines = CleanDocument.new(source: source).lines
|
||||||
expect(out.to_s).to eq(<<~EOM)
|
expect(lines[0].to_s).to eq($/)
|
||||||
|
expect(lines[1].to_s).to eq('puts "what"' + $/)
|
||||||
puts "what"
|
expect(lines[2].to_s).to eq($/)
|
||||||
|
|
||||||
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")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "trailing slash: does not join trailing do" do
|
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
|
# 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])
|
expect(code_lines.map(&:ignore_newline_not_beg?)).to eq([true, true, false, false])
|
||||||
end
|
end
|
||||||
|
|
||||||
it "trailing if" do
|
it "trailing if" do
|
||||||
code_lines = CodeLine.from_source(<<~'EOM')
|
code_lines = CodeLine.from_source(<<~'EOM')
|
||||||
puts "lol" if foo
|
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([
|
expect(io.string).to include([
|
||||||
" 1 class OH",
|
" 1 class OH",
|
||||||
"> 2 def hello",
|
"> 2 def hello",
|
||||||
|
" 3 def hai",
|
||||||
" 4 end",
|
" 4 end",
|
||||||
" 5 end",
|
" 5 end",
|
||||||
""
|
""
|
||||||
|
@ -162,6 +163,7 @@ module SyntaxSuggest
|
||||||
[
|
[
|
||||||
" 1 class OH",
|
" 1 class OH",
|
||||||
["> 2 ", DisplayCodeWithLineNumbers::TERMINAL_HIGHLIGHT, " def hello"].join,
|
["> 2 ", DisplayCodeWithLineNumbers::TERMINAL_HIGHLIGHT, " def hello"].join,
|
||||||
|
" 3 def hai",
|
||||||
" 4 end",
|
" 4 end",
|
||||||
" 5 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