217 lines
4.8 KiB
Crystal
217 lines
4.8 KiB
Crystal
require "../huffman"
|
|
require "./config"
|
|
require "./match_formatter"
|
|
require "./types"
|
|
|
|
module Fingers
|
|
struct Target
|
|
property text : String
|
|
property hint : String
|
|
property offset : Tuple(Int32, Int32)
|
|
|
|
def initialize(@text, @hint, @offset)
|
|
end
|
|
end
|
|
|
|
class Hinter
|
|
@formatter : Formatter
|
|
@patterns : Array(String)
|
|
@alphabet : Array(String)
|
|
@pattern : Regex | Nil
|
|
@hints : Array(String) | Nil
|
|
@n_matches : Int32 | Nil
|
|
@reuse_hints : Bool
|
|
|
|
def initialize(
|
|
input : Array(String),
|
|
width : Int32,
|
|
state : Fingers::State,
|
|
output : Printer,
|
|
patterns = Fingers.config.patterns,
|
|
alphabet = Fingers.config.alphabet,
|
|
huffman = Huffman.new,
|
|
formatter = ::Fingers::MatchFormatter.new,
|
|
reuse_hints = false
|
|
)
|
|
@lines = input
|
|
@width = width
|
|
@target_by_hint = {} of String => Target
|
|
@target_by_text = {} of String => Target
|
|
@state = state
|
|
@output = output
|
|
@formatter = formatter
|
|
@huffman = huffman
|
|
@patterns = patterns
|
|
@alphabet = alphabet
|
|
@reuse_hints = reuse_hints
|
|
end
|
|
|
|
def run
|
|
regenerate_hints! unless reuse_hints
|
|
lines[0..-2].each_with_index { |line, index| process_line(line, index, "\n") }
|
|
process_line(lines[-1], lines.size - 1, "")
|
|
|
|
output.flush
|
|
end
|
|
|
|
def lookup(hint) : Target | Nil
|
|
target_by_hint.fetch(hint) { nil }
|
|
end
|
|
|
|
# private
|
|
|
|
private getter :hints,
|
|
:hints_by_text,
|
|
:offsets_by_hint,
|
|
:input,
|
|
:lookup_table,
|
|
:width,
|
|
:state,
|
|
:formatter,
|
|
:huffman,
|
|
:output,
|
|
:patterns,
|
|
:alphabet,
|
|
:reuse_hints,
|
|
:target_by_hint,
|
|
:target_by_text
|
|
|
|
def process_line(line, line_index, ending)
|
|
result = line.gsub(pattern) { |_m| replace($~, line_index) }
|
|
result = Fingers.config.backdrop_style + result
|
|
double_width_correction = ((line.bytesize - line.size) / 3).round.to_i
|
|
padding_amount = (width - line.size - double_width_correction)
|
|
padding = padding_amount > 0 ? " " * padding_amount : ""
|
|
output.print(result + padding + ending)
|
|
end
|
|
|
|
def pattern : Regex
|
|
@pattern ||= Regex.new("(#{patterns.join('|')})")
|
|
end
|
|
|
|
def hints : Array(String)
|
|
return @hints.as(Array(String)) if !@hints.nil?
|
|
|
|
regenerate_hints!
|
|
|
|
@hints.as(Array(String))
|
|
end
|
|
|
|
def regenerate_hints!
|
|
@hints = huffman.generate_hints(alphabet: alphabet.clone, n: n_matches)
|
|
@target_by_hint.clear
|
|
@target_by_text.clear
|
|
end
|
|
|
|
def replace(match, line_index)
|
|
text = match[0]
|
|
|
|
captured_text = match["match"]? || text
|
|
capture_offset = capture_offset_for_match(match, captured_text)
|
|
|
|
hint = hint_for_text(text)
|
|
build_target(text, hint, {line_index, match.begin(0)})
|
|
|
|
if !state.input.empty? && !hint.starts_with?(state.input)
|
|
return text
|
|
end
|
|
|
|
formatter.format(
|
|
hint: hint,
|
|
highlight: text,
|
|
selected: state.selected_hints.includes?(hint),
|
|
offset: capture_offset
|
|
)
|
|
end
|
|
|
|
def hint_for_text(text)
|
|
return pop_hint! unless reuse_hints
|
|
|
|
target = target_by_text[text]?
|
|
|
|
if target.nil?
|
|
return pop_hint!
|
|
end
|
|
|
|
target.hint
|
|
end
|
|
|
|
def pop_hint! : String
|
|
hint = hints.pop?
|
|
|
|
if hint.nil?
|
|
raise "Too many matches"
|
|
end
|
|
|
|
hint
|
|
end
|
|
|
|
def capture_offset_for_match(match, captured_text)
|
|
return nil unless match["match"]?
|
|
|
|
match_start, match_end = {match.begin(0), match.end(0)}
|
|
capture_start, capture_end = find_capture_offset(match).not_nil!
|
|
{capture_start - match_start, captured_text.size}
|
|
end
|
|
|
|
def build_target(text, hint, offset)
|
|
target = Target.new(text, hint, offset)
|
|
|
|
target_by_hint[hint] = target
|
|
target_by_text[text] = target
|
|
|
|
target
|
|
end
|
|
|
|
def find_capture_offset(match : Regex::MatchData) : Tuple(Int32, Int32) | Nil
|
|
index = capture_indices.find { |i| match[i]? }
|
|
|
|
return nil unless index
|
|
|
|
{match.begin(index), match.end(index)}
|
|
end
|
|
|
|
getter capture_indices : Array(Int32) do
|
|
pattern.name_table.compact_map { |k, v| v == "match" ? k : nil }
|
|
end
|
|
|
|
def n_matches : Int32
|
|
return @n_matches.as(Int32) if !@n_matches.nil?
|
|
|
|
if reuse_hints
|
|
@n_matches = count_unique_matches
|
|
else
|
|
@n_matches = count_matches
|
|
end
|
|
end
|
|
|
|
def count_unique_matches
|
|
match_set = Set(String).new
|
|
|
|
lines.each do |line|
|
|
line.scan(pattern) do |match|
|
|
match_set.add(match[0]?.not_nil!)
|
|
end
|
|
end
|
|
|
|
@n_matches = match_set.size
|
|
|
|
match_set.size
|
|
end
|
|
|
|
def count_matches
|
|
result = 0
|
|
|
|
lines.each do |line|
|
|
line.scan(pattern) do |match|
|
|
result += 1
|
|
end
|
|
end
|
|
|
|
result
|
|
end
|
|
|
|
private property lines : Array(String)
|
|
end
|
|
end
|