├── .gitignore ├── Rakefile ├── lib ├── rtgrep │ ├── version.rb │ ├── searcher_field.rb │ ├── tags_file.rb │ ├── searcher_list_cell_renderer.rb │ ├── searcher_list.rb │ └── searcher.rb └── rtgrep.rb ├── rtgrep.gemspec └── bin └── rtgrep /.gitignore: -------------------------------------------------------------------------------- 1 | /pkg 2 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | -------------------------------------------------------------------------------- /lib/rtgrep/version.rb: -------------------------------------------------------------------------------- 1 | module Rtgrep 2 | VERSION = "0.0.1" 3 | end 4 | -------------------------------------------------------------------------------- /lib/rtgrep.rb: -------------------------------------------------------------------------------- 1 | require "rbcurse/core/util/app" 2 | 3 | module Rtgrep 4 | FILE_MARKER = "rtgrep file marker" 5 | end 6 | 7 | require "rtgrep/tags_file" 8 | require "rtgrep/searcher" 9 | require "rtgrep/searcher_list" 10 | require "rtgrep/searcher_list_cell_renderer" 11 | require "rtgrep/searcher_field" 12 | -------------------------------------------------------------------------------- /lib/rtgrep/searcher_field.rb: -------------------------------------------------------------------------------- 1 | class Rtgrep::SearcherField < RubyCurses::Field 2 | def searcher_list(list) 3 | @searcher_list = list 4 | end 5 | 6 | def handle_key(ch) 7 | if ((32..126).to_a + @key_handler.keys + [13]).include?(ch) 8 | super 9 | else 10 | @searcher_list.handle_key(ch) 11 | focus 12 | end 13 | end 14 | end 15 | 16 | -------------------------------------------------------------------------------- /rtgrep.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | require File.expand_path("../lib/rtgrep/version", __FILE__) 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "rtgrep" 7 | s.version = Rtgrep::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ['Brenton "B-Train" Fletcher'] 10 | s.email = ["i@bloople.net"] 11 | s.homepage = "http://github.com/bloopletech/rtgrep" 12 | s.summary = "Rtgrep lists vim tags and browses and selects them." 13 | s.description = "Rtgrep lists vim tags and browses and selects them." 14 | 15 | s.required_rubygems_version = ">= 1.3.6" 16 | s.rubyforge_project = "rtgrep" 17 | 18 | s.add_development_dependency "bundler", ">= 1.0.0" 19 | s.add_dependency "rbcurse-core", "= 0.0.3" 20 | 21 | s.files = `git ls-files`.split("\n") 22 | s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact 23 | s.require_path = 'lib' 24 | end 25 | -------------------------------------------------------------------------------- /lib/rtgrep/tags_file.rb: -------------------------------------------------------------------------------- 1 | class Rtgrep::TagsFile 2 | attr_reader :lines 3 | def initialize(lines, dir = nil) 4 | @lines = [] 5 | 6 | collection_name = false 7 | 8 | regex = /^#{Regexp.quote(dir + File::SEPARATOR)}/ if dir 9 | 10 | lines.each do |l| 11 | l.chomp! 12 | 13 | next if l == "" 14 | 15 | if !collection_name && l =~ /^\!_TAG_COLLECTION_NAME\t(.+)$/ 16 | collection_name = true 17 | @lines << [$1, Rtgrep::FILE_MARKER, "", "", ""] 18 | next 19 | end 20 | 21 | next if l =~ /^!_TAG_/ 22 | 23 | begin 24 | l =~ /^(.+?)\t(.+?)\t(.+?)(;"(.+)|)$/ 25 | l = [$1, $5, $3, $2, ""] #0 = tag, 1 = type, 2 = line num, 3 = path, 4 = line context 26 | 27 | extra = l[1] 28 | if extra 29 | extra =~ /^\t(.)/ 30 | type = $1 31 | l[1] = type 32 | end 33 | 34 | l[0].replace l[0][0..100] 35 | l[3].slice!(regex) if dir 36 | 37 | @lines << l 38 | rescue 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/rtgrep/searcher_list_cell_renderer.rb: -------------------------------------------------------------------------------- 1 | class Rtgrep::SearcherListCellRenderer < RubyCurses::ListCellRenderer 2 | def repaint graphic, r=@row,c=@col, row_index=-1,value=@text, focussed=false, selected=false 3 | if focussed 4 | offset = 236 5 | attr_offset = Ncurses::A_NORMAL 6 | else 7 | offset = 0 8 | attr_offset = Ncurses::A_NORMAL 9 | end 10 | 11 | chunk_line = nil 12 | 13 | if value[1] == Rtgrep::FILE_MARKER 14 | marker = "## #{value[0].upcase} " 15 | marker += "#" * [@display_length - marker.length, 0].max 16 | marker = marker[0..(@display_length)] 17 | chunk_line = Chunks::ChunkLine.new([Chunks::Chunk.new(ColorMap.get_color(252, offset), marker, Ncurses::A_BOLD)]) 18 | else 19 | chunks = [Chunks::Chunk.new(ColorMap.get_color(11, offset), " #{value[1]}", attr_offset), Chunks::Chunk.new(ColorMap.get_color(252, offset), value[0], Ncurses::A_BOLD)] 20 | 21 | if value[0] != value[3] 22 | chunks << Chunks::Chunk.new(ColorMap.get_color(245, offset), value[3], attr_offset) 23 | if value[2] != "1" 24 | chunks << Chunks::Chunk.new(ColorMap.get_color(245, offset), value[2], attr_offset) 25 | end 26 | end 27 | 28 | blank = Chunks::Chunk.new(ColorMap.get_color(252, offset), ' ', attr_offset) 29 | chunk_line = Chunks::ChunkLine.new(chunks.flat_map { |c| [c, blank] }[0...-1]) 30 | fill_length = (@display_length - chunk_line.length) 31 | chunk_line << Chunks::Chunk.new(ColorMap.get_color(252, offset), " " * fill_length, attr_offset) if fill_length > 0 32 | end 33 | 34 | graphic.wmove r, c 35 | graphic.show_colored_chunks chunk_line, ColorMap.get_color(238), nil 36 | end 37 | end 38 | 39 | -------------------------------------------------------------------------------- /lib/rtgrep/searcher_list.rb: -------------------------------------------------------------------------------- 1 | class Rtgrep::SearcherList < RubyCurses::List 2 | def convert_value_to_text(value, crow) 3 | value 4 | end 5 | 6 | def highlight_focussed_row type, r=nil, c=nil, acolor=nil 7 | return unless @should_show_focus 8 | case type 9 | when :FOCUSSED 10 | ix = @current_index 11 | return if is_row_selected ix 12 | r = _convert_index_to_printable_row() unless r 13 | focussed = true 14 | 15 | when :UNFOCUSSED 16 | return if @oldrow.nil? || @oldrow == @current_index 17 | ix = @oldrow 18 | return if is_row_selected ix 19 | r = _convert_index_to_printable_row(@oldrow) unless r 20 | return unless r # row is not longer visible 21 | focussed = false 22 | end 23 | unless c 24 | _r, c = rowcol 25 | end 26 | 27 | @cell_renderer.repaint(@graphic, r, c, ix, list()[ix], focussed, false) 28 | end 29 | 30 | =begin 31 | def highlight_selected_row r=nil, c=nil, acolor=nil 32 | return unless @selected_index # no selection 33 | r = _convert_index_to_printable_row(@selected_index) unless r 34 | return unless r # not on screen 35 | unless c 36 | _r, c = rowcol 37 | end 38 | STDERR.puts "r: #{r}, c: #{c}" 39 | @cell_renderer.repaint(@graphic, r, c, @selected_index, list()[@selected_index], false, true) 40 | end 41 | def unhighlight_row index, r=nil, c=nil, acolor=nil 42 | return unless index # no selection 43 | r = _convert_index_to_printable_row(index) unless r 44 | return unless r # not on screen 45 | unless c 46 | _r, c = rowcol 47 | end 48 | @cell_renderer.repaint(@graphic, r, c, index, list()[index], false, false) 49 | end 50 | =end 51 | 52 | def handle_key(ch) 53 | case ch 54 | when Ncurses::KEY_NPAGE, Ncurses::KEY_PPAGE 55 | if ch == Ncurses::KEY_NPAGE 56 | @toprow += height 57 | else 58 | @toprow -= height 59 | end 60 | @toprow = 0 if @toprow < 0 #The opposite case is handled inside bounds_check 61 | @oldrow = @current_index 62 | @current_index = @toprow 63 | bounds_check 64 | @repaint_required = true 65 | @widget_scrolled = true 66 | else 67 | super 68 | end 69 | end 70 | 71 | def on_enter_row arow 72 | super 73 | if current_value[1] == Rtgrep::FILE_MARKER 74 | if @current_index > @oldrow 75 | if next_row == :NO_NEXT_ROW 76 | previous_row 77 | end 78 | elsif @current_index < @oldrow 79 | if previous_row == :NO_PREVIOUS_ROW 80 | next_row 81 | end 82 | end 83 | end 84 | end 85 | 86 | def goto_top 87 | @current_index = -1 88 | super 89 | end 90 | end 91 | 92 | -------------------------------------------------------------------------------- /bin/rtgrep: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "rtgrep" 4 | 5 | $datacolor = $normalcolor = $def_fg_color = 238 6 | $actual_bg_color = 16 7 | $def_bg_color = $actual_bg_color + 0 8 | 9 | module ColorMap 10 | class << self 11 | alias_method :original_get_color_const, :get_color_const 12 | end 13 | def ColorMap.get_color_const colorstring 14 | if [:black, "black", 0, nil].include?(colorstring) 15 | return $actual_bg_color 16 | else 17 | original_get_color_const(colorstring) 18 | end 19 | end 20 | end 21 | 22 | $log = Logger.new("/dev/null") 23 | #$tlg = Logger.new("/home/bloopletech/tlg.log") 24 | 25 | App.new do 26 | @default_prefix = " " 27 | 28 | searcher = Rtgrep::Searcher.new 29 | ARGV.each do |a| 30 | searcher.lines += Rtgrep::TagsFile.new(File.readlines(a), Dir.getwd()).lines 31 | end 32 | searcher.update 33 | 34 | stack :margin_top => 0, :width => :expand, :height => FFI::NCurses.LINES, :color => $normalcolor, :bgcolor => $actual_bg_color do 35 | results_box = Rtgrep::SearcherList.new(nil, :list => searcher.search, :width => :expand, :height => FFI::NCurses.LINES - 2, :selection_mode => :single, :suppress_borders => true) 36 | _position(results_box) 37 | results_box.instance_variable_set("@display_length", FFI::NCurses.COLS) 38 | results_box.instance_variable_set("@internal_width", 0) 39 | results_box.cell_renderer(Rtgrep::SearcherListCellRenderer.new(:display_length => results_box.width)) 40 | 41 | label :text => " ", :width => :expand, :height => 1 42 | 43 | search_box = Rtgrep::SearcherField.new(nil, :label => "Search >", :searcher_list => results_box) 44 | _position(search_box) 45 | field_width = FFI::NCurses.COLS - search_box.label.length - 2 46 | search_box.instance_variable_set("@display_length", field_width) 47 | search_box.instance_variable_set("@maxlen", field_width) 48 | 49 | exit_proc = proc do 50 | throw(:close) 51 | end 52 | 53 | search_box.bind(:CHANGE) do |event| 54 | results_box.list(searcher.search(search_box.getvalue)) 55 | results_box.goto_top 56 | search_box.focus 57 | search_box.addcol(case event.type 58 | when :INSERT 59 | -1 60 | when :DELETE 61 | 1 62 | else 63 | nil 64 | end) 65 | end 66 | 67 | search_box.bind_key(13) do 68 | selected_tag = results_box.current_value 69 | 70 | if selected_tag 71 | STDERR.print "#{selected_tag[3]}\n#{selected_tag[2]}\n#{selected_tag[4]}\n" 72 | STDERR.flush 73 | end 74 | 75 | exit_proc.call 76 | end 77 | 78 | search_box.bind_key(2727) do 79 | exit_proc.call 80 | end 81 | 82 | if ENV["RTGREP_TAGGER"] 83 | search_box.bind_key(FFI::NCurses::KEY_F5) do 84 | system("#{ENV["RTGREP_TAGGER"]} >/dev/null") 85 | searcher.update 86 | results_box.list(searcher.search(search_box.getvalue)) 87 | search_box.fire_handler :CHANGE, InputDataEvent.new(nil, nil, nil, nil, nil, nil) 88 | end 89 | end 90 | 91 | @window.printstring(FFI::NCurses.LINES - 1, search_box.label.length, " ", $actual_bg_color) 92 | results_box.goto_top 93 | search_box.focus 94 | search_box.cursor_home 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/rtgrep/searcher.rb: -------------------------------------------------------------------------------- 1 | class Rtgrep::Searcher 2 | attr_accessor :lines 3 | def initialize 4 | @lines = [] 5 | update 6 | end 7 | 8 | def update 9 | if lines.empty? 10 | @longest_line_length = 0 11 | @longest_filename_length = 0 12 | else 13 | @longest_line_length = (@lines.max_by { |line| line[0].length }).first.length 14 | @longest_filename_length = (@lines.max_by { |line| line[3].length }).first.length 15 | end 16 | 17 | @last_matches = [] 18 | @last_needle = nil 19 | end 20 | 21 | def search(needle = nil) 22 | return @lines if @lines.empty? || !needle || needle.gsub(' ', '') == '' 23 | 24 | needle_parts = needle.split("").map do |c| 25 | c = Regexp.escape(c) 26 | "#{c}([^#{c}]*?)" 27 | end.join 28 | needle_regex = Regexp.new(needle_parts, "i") 29 | #needle_regex = Regexp.new(needle.split("").map { |c| "#{c}?[^#{c}]*?" }.join, "i") 30 | 31 | matching_lines = if @last_needle && needle.start_with?(@last_needle) 32 | #$tlg.error "Using last matches cache, last_needle: #{@last_needle.inspect}, needle: #{needle.inspect}, search space reduced from #{@haystack.length} to #{@last_matches.length}" 33 | @last_matches 34 | else 35 | #$tlg.error "SKIPPING last matches cache, last_needle: #{@last_needle.inspect}, needle: #{needle.inspect}, search space widened from #{@last_matches.length} to #{@haystack.length}" 36 | @lines.reject { |line| line[1] == Rtgrep::FILE_MARKER } 37 | end 38 | 39 | @last_needle = needle.dup 40 | @last_matches = matching_lines = matching_lines.select { |line| line[0] =~ needle_regex } 41 | 42 | matching_lines.sort_by do |line| 43 | shortest_match_offset = nil 44 | shortest_match_inbetweens = nil 45 | line[0].to_enum(:scan, needle_regex).map do 46 | match_data = Regexp.last_match 47 | 48 | offset = match_data.offset(0) 49 | if !shortest_match_offset || ((offset[1] - offset[0]) < (shortest_match_offset[1] - shortest_match_offset[0])) 50 | shortest_match_offset = offset 51 | shortest_match_inbetweens = match_data.captures.inject(0) { |sum, c| sum + c.length } 52 | end 53 | end 54 | 55 | match_line_length_mod = 1 - ((shortest_match_offset[1] - shortest_match_offset[0]) / line[0].length.to_f) 56 | match_inbetweens_length_mod = shortest_match_inbetweens / line[0].length.to_f 57 | match_position_mod = (shortest_match_offset[0] + 1) / line[0].length.to_f 58 | file_length_mod = line[3].length / @longest_filename_length.to_f 59 | line_length_mod = line[0].length / @longest_line_length.to_f 60 | 61 | score = (match_line_length_mod * 10.0) + (match_inbetweens_length_mod * 10.0) + (match_position_mod * 10.0) + (file_length_mod * 2.0) + (line_length_mod * 2.0) 62 | 63 | # $tlg.debug "\n\n\n\nNeedle: #{needle.inspect}\nLine:\n#{line.inspect}\nShortest match offset: #{shortest_match_offset.inspect}\nShortest match inbetweens: #{shortest_match_inbetweens.inspect}\nmatch_length_mod: #{match_length_mod}, match_inbetweens_length_mod: #{match_inbetweens_length_mod}, file_length_mod: #{file_length_mod}, match_line_length_mod: #{match_line_length_mod}, match_position_mod: #{match_position_mod}, line_length_mod: #{line_length_mod}\nResult: #{result}" 64 | 65 | score 66 | end 67 | end 68 | end 69 | 70 | --------------------------------------------------------------------------------