Skip to content

Commit

Permalink
Bump up syntax_suggest-1.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
hsbt authored and nagachika committed Aug 20, 2023
1 parent 0c908fa commit d583792
Show file tree
Hide file tree
Showing 27 changed files with 968 additions and 206 deletions.
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
Loading

0 comments on commit d583792

Please sign in to comment.