summaryrefslogtreecommitdiff
path: root/lib/syntax_suggest/code_search.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/syntax_suggest/code_search.rb')
-rw-r--r--lib/syntax_suggest/code_search.rb139
1 files changed, 139 insertions, 0 deletions
diff --git a/lib/syntax_suggest/code_search.rb b/lib/syntax_suggest/code_search.rb
new file mode 100644
index 0000000000..7628dcd131
--- /dev/null
+++ b/lib/syntax_suggest/code_search.rb
@@ -0,0 +1,139 @@
+# frozen_string_literal: true
+
+module SyntaxSuggest
+ # Searches code for a syntax error
+ #
+ # There are three main phases in the algorithm:
+ #
+ # 1. Sanitize/format input source
+ # 2. Search for invalid blocks
+ # 3. Format invalid blocks into something meaninful
+ #
+ # This class handles the part.
+ #
+ # The bulk of the heavy lifting is done in:
+ #
+ # - CodeFrontier (Holds information for generating blocks and determining if we can stop searching)
+ # - ParseBlocksFromLine (Creates blocks into the frontier)
+ # - BlockExpand (Expands existing blocks to search more code)
+ #
+ # ## Syntax error detection
+ #
+ # When the frontier holds the syntax error, we can stop searching
+ #
+ # search = CodeSearch.new(<<~EOM)
+ # def dog
+ # def lol
+ # end
+ # EOM
+ #
+ # search.call
+ #
+ # search.invalid_blocks.map(&:to_s) # =>
+ # # => ["def lol\n"]
+ #
+ class CodeSearch
+ private
+
+ attr_reader :frontier
+
+ public
+
+ attr_reader :invalid_blocks, :record_dir, :code_lines
+
+ def initialize(source, record_dir: DEFAULT_VALUE)
+ record_dir = if record_dir == DEFAULT_VALUE
+ (ENV["SYNTAX_SUGGEST_RECORD_DIR"] || ENV["SYNTAX_SUGGEST_DEBUG"]) ? "tmp" : nil
+ else
+ record_dir
+ end
+
+ if record_dir
+ @record_dir = SyntaxSuggest.record_dir(record_dir)
+ @write_count = 0
+ end
+
+ @tick = 0
+ @source = source
+ @name_tick = Hash.new { |hash, k| hash[k] = 0 }
+ @invalid_blocks = []
+
+ @code_lines = CleanDocument.new(source: source).call.lines
+
+ @frontier = CodeFrontier.new(code_lines: @code_lines)
+ @block_expand = BlockExpand.new(code_lines: @code_lines)
+ @parse_blocks_from_indent_line = ParseBlocksFromIndentLine.new(code_lines: @code_lines)
+ end
+
+ # Used for debugging
+ def record(block:, name: "record")
+ return unless @record_dir
+ @name_tick[name] += 1
+ filename = "#{@write_count += 1}-#{name}-#{@name_tick[name]}-(#{block.starts_at}__#{block.ends_at}).txt"
+ if ENV["SYNTAX_SUGGEST_DEBUG"]
+ puts "\n\n==== #{filename} ===="
+ puts "\n```#{block.starts_at}..#{block.ends_at}"
+ puts block
+ puts "```"
+ puts " block indent: #{block.current_indent}"
+ end
+ @record_dir.join(filename).open(mode: "a") do |f|
+ document = DisplayCodeWithLineNumbers.new(
+ lines: @code_lines.select(&:visible?),
+ terminal: false,
+ highlight_lines: block.lines
+ ).call
+
+ f.write(" Block lines: #{block.starts_at..block.ends_at} (#{name}) \n\n#{document}")
+ end
+ end
+
+ def push(block, name:)
+ record(block: block, name: name)
+
+ block.mark_invisible if block.valid?
+ frontier << block
+ end
+
+ # Parses the most indented lines into blocks that are marked
+ # and added to the frontier
+ def create_blocks_from_untracked_lines
+ max_indent = frontier.next_indent_line&.indent
+
+ while (line = frontier.next_indent_line) && (line.indent == max_indent)
+ @parse_blocks_from_indent_line.each_neighbor_block(frontier.next_indent_line) do |block|
+ push(block, name: "add")
+ end
+ end
+ end
+
+ # Given an already existing block in the frontier, expand it to see
+ # if it contains our invalid syntax
+ def expand_existing
+ block = frontier.pop
+ return unless block
+
+ record(block: block, name: "before-expand")
+
+ block = @block_expand.call(block)
+ push(block, name: "expand")
+ end
+
+ # Main search loop
+ def call
+ until frontier.holds_all_syntax_errors?
+ @tick += 1
+
+ if frontier.expand?
+ expand_existing
+ else
+ create_blocks_from_untracked_lines
+ end
+ end
+
+ @invalid_blocks.concat(frontier.detect_invalid_blocks)
+ @invalid_blocks.sort_by! { |block| block.starts_at }
+ self
+ end
+ end
+end