From fcc4a3ad6c46b13b1cd6bf352129cf4dd97b6839 Mon Sep 17 00:00:00 2001 From: Jorge Morante Date: Fri, 10 Nov 2023 10:13:49 +0100 Subject: [PATCH] adding jump mode functionality --- spec/lib/fingers/hinter_spec.cr | 63 ++++++++++--- src/fingers/action_runner.cr | 19 +++- src/fingers/commands/load_config.cr | 4 +- src/fingers/commands/start.cr | 10 +- src/fingers/config.cr | 2 + src/fingers/hinter.cr | 139 ++++++++++++++++++++-------- src/fingers/view.cr | 19 ++-- 7 files changed, 193 insertions(+), 63 deletions(-) diff --git a/spec/lib/fingers/hinter_spec.cr b/spec/lib/fingers/hinter_spec.cr index c53b313..9f55c47 100644 --- a/spec/lib/fingers/hinter_spec.cr +++ b/spec/lib/fingers/hinter_spec.cr @@ -46,13 +46,6 @@ describe Fingers::Hinter do alphabet: alphabet, output: output, ) - - puts "before" - puts input - puts "---------" - puts "after" - hinter.run - puts output.contents end it "only highlights captured groups" do @@ -89,12 +82,58 @@ Changes not staged for commit: alphabet: alphabet, output: output, ) + end + + it "only reuses hints when allow duplicates is false" do + width = 100 + output = TextOutput.new + + patterns = Fingers::Config::DEFAULT_PATTERNS.values.to_a + alphabet = "asdf".split("") + + input = " + modified: src/fingers/cli.cr + modified: src/fingers/cli.cr + modified: src/fingers/cli.cr + " + + hinter = Fingers::Hinter.new( + input: input.split("\n"), + width: width, + patterns: patterns, + state: ::Fingers::State.new, + alphabet: alphabet, + output: output, + reuse_hints: false + ) - puts "before" - puts input - puts "---------" - puts "after" hinter.run - puts output.contents + end + + it "can rerender when not reusing hints" do + width = 100 + output = TextOutput.new + + patterns = Fingers::Config::DEFAULT_PATTERNS.values.to_a + alphabet = "asdf".split("") + + input = " + modified: src/fingers/cli.cr + modified: src/fingers/cli.cr + modified: src/fingers/cli.cr + " + + hinter = Fingers::Hinter.new( + input: input.split("\n"), + width: width, + patterns: patterns, + state: ::Fingers::State.new, + alphabet: alphabet, + output: output, + reuse_hints: false + ) + + hinter.run + hinter.run end end diff --git a/src/fingers/action_runner.cr b/src/fingers/action_runner.cr index 1150ff9..cb59adb 100644 --- a/src/fingers/action_runner.cr +++ b/src/fingers/action_runner.cr @@ -4,7 +4,7 @@ module Fingers class ActionRunner @final_shell_command : String | Nil - def initialize(@modifier : String, @match : String, @hint : String, @original_pane : Tmux::Pane) + def initialize(@modifier : String, @match : String, @hint : String, @original_pane : Tmux::Pane, @offset : Tuple(Int32, Int32) | Nil, @mode : String) end def run @@ -28,9 +28,10 @@ module Fingers cmd.input.flush end - private getter :match, :modifier, :hint, :original_pane + private getter :match, :modifier, :hint, :original_pane, :offset, :mode def final_shell_command + return jump if mode == "jump" return @final_shell_command if @final_shell_command @final_shell_command = case action @@ -40,6 +41,8 @@ module Fingers open when ":paste:" paste + when ":jump:" + jump when nil # do nothing else @@ -59,6 +62,18 @@ module Fingers system_open_command end + def jump + return nil if offset.nil? + + `tmux copy-mode -t #{original_pane.pane_id}` + `tmux send-keys -t #{original_pane.pane_id} -X start-of-line` + `tmux send-keys -t #{original_pane.pane_id} -X top-line` + `tmux send-keys -t #{original_pane.pane_id} -N #{offset.not_nil![0]} -X cursor-down` + `tmux send-keys -t #{original_pane.pane_id} -N #{offset.not_nil![1]} -X cursor-right` + + nil + end + def paste "tmux paste-buffer" end diff --git a/src/fingers/commands/load_config.cr b/src/fingers/commands/load_config.cr index 2dfc8af..cecfd0d 100644 --- a/src/fingers/commands/load_config.cr +++ b/src/fingers/commands/load_config.cr @@ -38,6 +38,8 @@ class Fingers::Commands::LoadConfig < Fingers::Commands::Base case option when "key" config.key = value + when "jump_key" + config.jump_key = value when "keyboard_layout" config.keyboard_layout = value when "main_action" @@ -95,7 +97,7 @@ class Fingers::Commands::LoadConfig < Fingers::Commands::Base def setup_bindings `tmux bind-key #{Fingers.config.key} run-shell -b "#{cli} start "\#{pane_id}" self >>#{Fingers::Dirs::LOG_PATH} 2>&1"` - `tmux bind-key O run-shell -b "#{cli} start "\#{pane_id}" other >>#{Fingers::Dirs::LOG_PATH} 2>&1"` + `tmux bind-key #{Fingers.config.jump_key} run-shell -b "#{cli} start "\#{pane_id}" jump >>#{Fingers::Dirs::LOG_PATH} 2>&1"` setup_fingers_mode_bindings end diff --git a/src/fingers/commands/start.cr b/src/fingers/commands/start.cr index b84cfe0..d6e0066 100644 --- a/src/fingers/commands/start.cr +++ b/src/fingers/commands/start.cr @@ -102,6 +102,10 @@ module Fingers::Commands tmux.find_pane_by_id(@args[0]).not_nil! end + private getter mode : String do + @args[1].not_nil! + end + private getter fingers_window : Tmux::Window do tmux.create_window("[fingers]", "cat", 80, 24) end @@ -123,7 +127,8 @@ module Fingers::Commands input: pane_contents, width: target_pane.pane_width.to_i, state: state, - output: pane_printer + output: pane_printer, + reuse_hints: mode != "jump", ) end @@ -137,7 +142,8 @@ module Fingers::Commands state: state, output: pane_printer, original_pane: target_pane, - tmux: tmux + tmux: tmux, + mode: mode ) end diff --git a/src/fingers/config.cr b/src/fingers/config.cr index b53dbaa..9791166 100644 --- a/src/fingers/config.cr +++ b/src/fingers/config.cr @@ -5,6 +5,7 @@ module Fingers include JSON::Serializable property key : String + property jump_key : String property keyboard_layout : String property patterns : Array(String) property alphabet : Array(String) @@ -63,6 +64,7 @@ module Fingers def initialize( @key = "F", + @jump_key = "J", @keyboard_layout = "qwerty", @alphabet = [] of String, @patterns = [] of String, diff --git a/src/fingers/hinter.cr b/src/fingers/hinter.cr index 279143f..35fc0c4 100644 --- a/src/fingers/hinter.cr +++ b/src/fingers/hinter.cr @@ -4,6 +4,15 @@ 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) @@ -11,6 +20,7 @@ module Fingers @pattern : Regex | Nil @hints : Array(String) | Nil @n_matches : Int32 | Nil + @reuse_hints : Bool def initialize( input : Array(String), @@ -20,42 +30,39 @@ module Fingers patterns = Fingers.config.patterns, alphabet = Fingers.config.alphabet, huffman = Huffman.new, - formatter = ::Fingers::MatchFormatter.new + formatter = ::Fingers::MatchFormatter.new, + reuse_hints = false ) @lines = input @width = width - @hints_by_text = {} of String => String - @lookup_table = {} of String => String + @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 - lines[0..-2].each { |line| process_line(line, "\n") } - process_line(lines[-1], "") + 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, "") - # STDOUT.flush output.flush - - build_lookup_table! end - def lookup(hint) - lookup_table.fetch(hint) { nil } - end - - def matches - @matches ||= @hints_by_text.keys.uniq!.flatten + 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, @@ -64,14 +71,13 @@ module Fingers :huffman, :output, :patterns, - :alphabet + :alphabet, + :reuse_hints, + :target_by_hint, + :target_by_text - def build_lookup_table! - @lookup_table = hints_by_text.invert - end - - def process_line(line, ending) - result = line.gsub(pattern) { |_m| replace($~) } + 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) @@ -86,31 +92,25 @@ module Fingers def hints : Array(String) return @hints.as(Array(String)) if !@hints.nil? - @hints = huffman.generate_hints(alphabet: alphabet, n: n_matches) + regenerate_hints! + + @hints.as(Array(String)) end - def replace(match) + 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) - if match["match"]? - match_start, match_end = {match.begin(0), match.end(0)} - capture_start, capture_end = find_capture_offset(match).not_nil! - capture_offset = {capture_start - match_start, captured_text.size} - else - capture_offset = nil - end - - if hints_by_text.has_key?(captured_text) - hint = hints_by_text[captured_text] - else - hint = hints.pop - - raise "Too many matches" if hint.nil? - - hints_by_text[captured_text] = hint - end + 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 @@ -124,6 +124,45 @@ module Fingers ) 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]? } @@ -139,6 +178,14 @@ module Fingers 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| @@ -152,6 +199,18 @@ module Fingers 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 diff --git a/src/fingers/view.cr b/src/fingers/view.cr index 56d1303..996889a 100644 --- a/src/fingers/view.cr +++ b/src/fingers/view.cr @@ -13,13 +13,15 @@ module Fingers @output : Printer @original_pane : Tmux::Pane @tmux : Tmux + @mode : String def initialize( @hinter, @output, @original_pane, @state, - @tmux + @tmux, + @mode ) end @@ -47,11 +49,15 @@ module Fingers end def run_action + match = hinter.lookup(state.input) + ActionRunner.new( hint: state.input, modifier: state.modifier, match: state.result, - original_pane: original_pane + original_pane: original_pane, + offset: match ? match.not_nil!.offset : nil, + mode: mode ).run tmux.display_message("Copied: #{state.result}", 1000) if should_notify? @@ -68,12 +74,13 @@ module Fingers private def process_hint(char, modifier) state.input += char state.modifier = modifier + match = hinter.lookup(state.input) - if match - handle_match(match) - else + if match.nil? render + else + handle_match(match.not_nil!.text) end end @@ -88,7 +95,7 @@ module Fingers end end - private getter :output, :hinter, :original_pane, :state, :tmux + private getter :output, :hinter, :original_pane, :state, :tmux, :mode private def handle_match(match) if state.multi_mode