module CodeRay module Scanners # Scanner for output of the diff command. # # Alias: +patch+ class Diff < Scanner register_for :diff title 'diff output' DEFAULT_OPTIONS = { :highlight_code => true, :inline_diff => true, } protected def scan_tokens encoder, options line_kind = nil state = :initial deleted_lines_count = 0 scanners = Hash.new do |h, lang| h[lang] = Scanners[lang].new '', :keep_tokens => true, :keep_state => true end content_scanner = scanners[:plain] content_scanner_entry_state = nil until eos? if match = scan(/\n/) deleted_lines_count = 0 unless line_kind == :delete if line_kind encoder.end_line line_kind line_kind = nil end encoder.text_token match, :space next end case state when :initial if match = scan(/--- |\+\+\+ |=+|_+/) encoder.begin_line line_kind = :head encoder.text_token match, :head if match = scan(/[^\x00\n]+?(?=$|[\t\n]| \(revision)/) encoder.text_token match, :filename if options[:highlight_code] && match != '/dev/null' file_type = CodeRay::FileType.fetch(match, :text) file_type = :text if file_type == :diff content_scanner = scanners[file_type] content_scanner_entry_state = nil end end next unless match = scan(/.+/) encoder.text_token match, :plain elsif match = scan(/Index: |Property changes on: /) encoder.begin_line line_kind = :head encoder.text_token match, :head next unless match = scan(/.+/) encoder.text_token match, :plain elsif match = scan(/Added: /) encoder.begin_line line_kind = :head encoder.text_token match, :head next unless match = scan(/.+/) encoder.text_token match, :plain state = :added elsif match = scan(/\\ .*/) encoder.text_token match, :comment elsif match = scan(/@@(?>[^@\n]+)@@/) content_scanner.state = :initial unless match?(/\n\+/) content_scanner_entry_state = nil if check(/\n|$/) encoder.begin_line line_kind = :change else encoder.begin_group :change end encoder.text_token match[0,2], :change encoder.text_token match[2...-2], :plain encoder.text_token match[-2,2], :change encoder.end_group :change unless line_kind next unless match = scan(/.+/) if options[:highlight_code] content_scanner.tokenize match, :tokens => encoder else encoder.text_token match, :plain end next elsif match = scan(/\+/) encoder.begin_line line_kind = :insert encoder.text_token match, :insert next unless match = scan(/.+/) if options[:highlight_code] content_scanner.tokenize match, :tokens => encoder else encoder.text_token match, :plain end next elsif match = scan(/-/) deleted_lines_count += 1 if options[:inline_diff] && deleted_lines_count == 1 && (changed_lines_count = 1 + check(/.*(?:\n\-.*)*/).count("\n")) && match?(/(?>.*(?:\n\-.*){#{changed_lines_count - 1}}(?:\n\+.*){#{changed_lines_count}})$(?!\n\+)/) deleted_lines = Array.new(changed_lines_count) { |i| skip(/\n\-/) if i > 0; scan(/.*/) } inserted_lines = Array.new(changed_lines_count) { |i| skip(/\n\+/) ; scan(/.*/) } deleted_lines_tokenized = [] inserted_lines_tokenized = [] for deleted_line, inserted_line in deleted_lines.zip(inserted_lines) pre, deleted_part, inserted_part, post = diff deleted_line, inserted_line content_scanner_entry_state = content_scanner.state deleted_lines_tokenized << content_scanner.tokenize([pre, deleted_part, post], :tokens => Tokens.new) content_scanner.state = content_scanner_entry_state || :initial inserted_lines_tokenized << content_scanner.tokenize([pre, inserted_part, post], :tokens => Tokens.new) end for pre, deleted_part, post in deleted_lines_tokenized encoder.begin_line :delete encoder.text_token '-', :delete encoder.tokens pre unless deleted_part.empty? encoder.begin_group :eyecatcher encoder.tokens deleted_part encoder.end_group :eyecatcher end encoder.tokens post encoder.end_line :delete encoder.text_token "\n", :space end for pre, inserted_part, post in inserted_lines_tokenized encoder.begin_line :insert encoder.text_token '+', :insert encoder.tokens pre unless inserted_part.empty? encoder.begin_group :eyecatcher encoder.tokens inserted_part encoder.end_group :eyecatcher end encoder.tokens post changed_lines_count -= 1 if changed_lines_count > 0 encoder.end_line :insert encoder.text_token "\n", :space end end line_kind = :insert elsif match = scan(/.*/) encoder.begin_line line_kind = :delete encoder.text_token '-', :delete if options[:highlight_code] if deleted_lines_count == 1 content_scanner_entry_state = content_scanner.state end content_scanner.tokenize match, :tokens => encoder unless match.empty? if !match?(/\n-/) if match?(/\n\+/) content_scanner.state = content_scanner_entry_state || :initial end content_scanner_entry_state = nil end else encoder.text_token match, :plain end end next elsif match = scan(/ .*/) if options[:highlight_code] content_scanner.tokenize match, :tokens => encoder else encoder.text_token match, :plain end next elsif match = scan(/.+/) encoder.begin_line line_kind = :comment encoder.text_token match, :plain else raise_inspect 'else case rached' end when :added if match = scan(/ \+/) encoder.begin_line line_kind = :insert encoder.text_token match, :insert next unless match = scan(/.+/) encoder.text_token match, :plain else state = :initial next end end end encoder.end_line line_kind if line_kind encoder end private def diff a, b # i will be the index of the leftmost difference from the left. i_max = [a.size, b.size].min i = 0 i += 1 while i < i_max && a[i] == b[i] # j_min will be the index of the leftmost difference from the right. j_min = i - i_max # j will be the index of the rightmost difference from the right which # does not precede the leftmost one from the left. j = -1 j -= 1 while j >= j_min && a[j] == b[j] return a[0...i], a[i..j], b[i..j], (j < -1) ? a[j+1..-1] : '' end end end end