Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge syntax_suggest-1.1.0 #7885

Merged
merged 1 commit into from
Aug 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 4 additions & 2 deletions lib/syntax_suggest/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def self.call(source:, filename: DEFAULT_VALUE, terminal: DEFAULT_VALUE, record_
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

Expand All @@ -91,7 +91,9 @@ def self.record_dir(dir)
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

Expand Down
290 changes: 149 additions & 141 deletions lib/syntax_suggest/around_block_scan.rb
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -24,201 +26,207 @@ module SyntaxSuggest
# puts scan.before_index # => 0
# puts scan.after_index # => 3
#
# Contents can also be filtered using AroundBlockScan#skip
#
# To grab the next surrounding indentation use AroundBlockScan#scan_adjacent_indent
class AroundBlockScan
def initialize(code_lines:, block:)
@code_lines = code_lines
@orig_before_index = block.lines.first.index
@orig_after_index = block.lines.last.index
@orig_indent = block.current_indent
@skip_array = []
@after_array = []
@before_array = []
@stop_after_kw = false

@skip_hidden = false
@skip_empty = false
@stop_after_kw = false
@force_add_empty = false
@force_add_hidden = false
@target_indent = nil

@scanner = ScanHistory.new(code_lines: code_lines, block: block)
end

# When using this flag, `scan_while` will
# bypass the block it's given and always add a
# line that responds truthy to `CodeLine#hidden?`
#
# Lines are hidden when they've been evaluated by
# the parser as part of a block and found to contain
# valid code.
def force_add_hidden
@force_add_hidden = true
self
end

def skip(name)
case name
when :hidden?
@skip_hidden = true
when :empty?
@skip_empty = true
else
raise "Unsupported skip #{name}"
end
# When using this flag, `scan_while` will
# bypass the block it's given and always add a
# line that responds truthy to `CodeLine#empty?`
#
# Empty lines contain no code, only whitespace such
# as leading spaces a newline.
def force_add_empty
@force_add_empty = true
self
end

# Tells `scan_while` to look for mismatched keyword/end-s
#
# When scanning up, if we see more keywords then end-s it will
# stop. This might happen when scanning outside of a method body.
# the first scan line up would be a keyword and this setting would
# trigger a stop.
#
# When scanning down, stop if there are more end-s than keywords.
def stop_after_kw
@stop_after_kw = true
self
end

# Main work method
#
# The scan_while method takes a block that yields lines above and
# below the block. If the yield returns true, the @before_index
# or @after_index are modified to include the matched line.
#
# In addition to yielding individual lines, the internals of this
# object give a mini DSL to handle common situations such as
# stopping if we've found a keyword/end mis-match in one direction
# or the other.
def scan_while
stop_next = false

kw_count = 0
end_count = 0
index = before_lines.reverse_each.take_while do |line|
next false if stop_next
next true if @skip_hidden && line.hidden?
next true if @skip_empty && line.empty?
stop_next_up = false
stop_next_down = false

kw_count += 1 if line.is_kw?
end_count += 1 if line.is_end?
if @stop_after_kw && kw_count > end_count
stop_next = true
end

yield line
end.last&.index
@scanner.scan(
up: ->(line, kw_count, end_count) {
next false if stop_next_up
next true if @force_add_hidden && line.hidden?
next true if @force_add_empty && line.empty?

if index && index < before_index
@before_index = index
end
if @stop_after_kw && kw_count > end_count
stop_next_up = true
end

stop_next = false
kw_count = 0
end_count = 0
index = after_lines.take_while do |line|
next false if stop_next
next true if @skip_hidden && line.hidden?
next true if @skip_empty && line.empty?
yield line
},
down: ->(line, kw_count, end_count) {
next false if stop_next_down
next true if @force_add_hidden && line.hidden?
next true if @force_add_empty && line.empty?

kw_count += 1 if line.is_kw?
end_count += 1 if line.is_end?
if @stop_after_kw && end_count > kw_count
stop_next = true
end
if @stop_after_kw && end_count > kw_count
stop_next_down = true
end

yield line
end.last&.index
yield line
}
)

if index && index > after_index
@after_index = index
end
self
end

def capture_neighbor_context
lines = []
# Scanning is intentionally conservative because
# we have no way of rolling back an agressive block (at this time)
#
# If a block was stopped for some trivial reason, (like an empty line)
# but the next line would have caused it to be balanced then we
# can check that condition and grab just one more line either up or
# down.
#
# For example, below if we're scanning up, line 2 might cause
# the scanning to stop. This is because empty lines might
# denote logical breaks where the user intended to chunk code
# which is a good place to stop and check validity. Unfortunately
# it also means we might have a "dangling" keyword or end.
#
# 1 def bark
# 2
# 3 end
#
# If lines 2 and 3 are in the block, then when this method is
# run it would see it is unbalanced, but that acquiring line 1
# would make it balanced, so that's what it does.
def lookahead_balance_one_line
kw_count = 0
end_count = 0
before_lines.reverse_each do |line|
next if line.empty?
break if line.indent < @orig_indent
next if line.indent != @orig_indent

lines.each do |line|
kw_count += 1 if line.is_kw?
end_count += 1 if line.is_end?
if kw_count != 0 && kw_count == end_count
lines << line
break
end

lines << line
end

lines.reverse!

kw_count = 0
end_count = 0
after_lines.each do |line|
next if line.empty?
break if line.indent < @orig_indent
next if line.indent != @orig_indent

kw_count += 1 if line.is_kw?
end_count += 1 if line.is_end?
if kw_count != 0 && kw_count == end_count
lines << line
break
return self if kw_count == end_count # nothing to balance

@scanner.commit_if_changed # Rollback point if we don't find anything to optimize

# Try to eat up empty lines
@scanner.scan(
up: ->(line, _, _) { line.hidden? || line.empty? },
down: ->(line, _, _) { line.hidden? || line.empty? }
)

# More ends than keywords, check if we can balance expanding up
next_up = @scanner.next_up
next_down = @scanner.next_down
case end_count - kw_count
when 1
if next_up&.is_kw? && next_up.indent >= @target_indent
@scanner.scan(
up: ->(line, _, _) { line == next_up },
down: ->(line, _, _) { false }
)
@scanner.commit_if_changed
end

lines << line
end

lines
end

def on_falling_indent
last_indent = @orig_indent
before_lines.reverse_each do |line|
next if line.empty?
if line.indent < last_indent
yield line
last_indent = line.indent
end
end

last_indent = @orig_indent
after_lines.each do |line|
next if line.empty?
if line.indent < last_indent
yield line
last_indent = line.indent
when -1
if next_down&.is_end? && next_down.indent >= @target_indent
@scanner.scan(
up: ->(line, _, _) { false },
down: ->(line, _, _) { line == next_down }
)
@scanner.commit_if_changed
end
end
end

def scan_neighbors
scan_while { |line| line.not_empty? && line.indent >= @orig_indent }
end
# Rollback any uncommitted changes
@scanner.stash_changes

def next_up
@code_lines[before_index.pred]
self
end

def next_down
@code_lines[after_index.next]
# Finds code lines at the same or greater indentation and adds them
# to the block
def scan_neighbors_not_empty
@target_indent = @orig_indent
scan_while { |line| line.not_empty? && line.indent >= @target_indent }
end

# Scan blocks based on indentation of next line above/below block
#
# Determines indentaion of the next line above/below the current block.
#
# Normally this is called when a block has expanded to capture all "neighbors"
# at the same (or greater) indentation and needs to expand out. For example
# the `def/end` lines surrounding a method.
def scan_adjacent_indent
before_after_indent = []
before_after_indent << (next_up&.indent || 0)
before_after_indent << (next_down&.indent || 0)

indent = before_after_indent.min
scan_while { |line| line.not_empty? && line.indent >= indent }
before_after_indent << (@scanner.next_up&.indent || 0)
before_after_indent << (@scanner.next_down&.indent || 0)

self
end
@target_indent = before_after_indent.min
scan_while { |line| line.not_empty? && line.indent >= @target_indent }

def start_at_next_line
before_index
after_index
@before_index -= 1
@after_index += 1
self
end

# Return the currently matched lines as a `CodeBlock`
#
# When a `CodeBlock` is created it will gather metadata about
# itself, so this is not a free conversion. Avoid allocating
# more CodeBlock's than needed
def code_block
CodeBlock.new(lines: lines)
end

# Returns the lines matched by the current scan as an
# array of CodeLines
def lines
@code_lines[before_index..after_index]
end

def before_index
@before_index ||= @orig_before_index
end

def after_index
@after_index ||= @orig_after_index
end

private def before_lines
@code_lines[0...before_index] || []
@scanner.lines
end

private def after_lines
@code_lines[after_index.next..-1] || []
# Managable rspec errors
def inspect
"#<#{self.class}:0x0000123843lol >"
end
end
end