Module: Railbow::NotesFormatter

Defined in:
lib/railbow/notes_formatter.rb

Constant Summary collapse

RESET =
Formatters::Base::RESET
BOLD =
Formatters::Base::BOLD
DIM =
Formatters::Base::DIM
RED =
Formatters::Base::RED
YELLOW =
Formatters::Base::YELLOW
CYAN =
Formatters::Base::CYAN
GREEN =
Formatters::Base::GREEN
WHITE =
Formatters::Base::BRIGHT_WHITE
BAR =

"\u258e"
TAG_COLORS =
{
  "TODO" => YELLOW,
  "FIXME" => RED,
  "OPTIMIZE" => CYAN,
  "HACK" => RED,
  "NOTE" => GREEN
}.freeze
PALETTE =
Formatters::Base::TABLE_PALETTE

Instance Method Summary collapse

Instance Method Details

#display(results, options = {}) ⇒ Object



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/railbow/notes_formatter.rb', line 33

def display(results, options = {})
  return super if Railbow.plain?

  if Railbow::Params.help?
    print_help
    return
  end

  author_mode = Railbow::Params.git_author
  since_val = Railbow::Params.since
  sort_mode = Railbow::Params.sort

  since_date = Railbow::Params.parse_since(since_val, context: "annotations")
  git_needed = %w[all me].include?(author_mode) || since_date || sort_mode == "date"

  # Build blame cache for all files if git features are needed
  blame_cache = {}
  if git_needed
    results.each_key do |source_file|
      blame_cache[source_file] ||= blame_file(source_file)
    end
  end

  # Flatten annotations into a list for filtering/sorting
  entries = []
  results.each do |source_file, annotations|
    annotations.each do |annotation|
      blame_info = blame_cache.dig(source_file, annotation.line)
      entries << {
        file: source_file,
        annotation: annotation,
        blame: blame_info
      }
    end
  end

  # Filter by SINCE
  skipped = 0
  if since_date
    before = entries.size
    entries = entries.select do |entry|
      entry[:blame] && entry[:blame][:date] && entry[:blame][:date] >= since_date
    end
    skipped = before - entries.size
  end

  # Sort by date (newest first) when requested
  if sort_mode == "date"
    entries.sort_by! do |entry|
      d = entry.dig(:blame, :date)
      d ? -d.to_time.to_i : 0
    end
  end

  # Determine "me" identity
  git_email = nil
  if author_mode == "me"
    git_email = current_git_email
  end

  # Determine max location width for alignment when showing metadata
  show_date = git_needed
  max_loc_width = 0
  author_colors = nil
  author_display = {}
  if author_mode == "all"
    author_names = entries.filter_map { |e| e.dig(:blame, :author) }.uniq
    author_colors = ColorAssigner.new(author_names)
    author_display = Railbow::Params.format_authors(author_names)
  end
  if show_date || author_mode == "all"
    entries.each do |entry|
      loc = "#{entry[:file]}:#{entry[:annotation].line}"
      max_loc_width = loc.length if loc.length > max_loc_width
    end
  end

  # Group entries back by file for bar coloring
  file_counts = Hash.new(0)
  entries.each { |e| file_counts[e[:file]] += 1 }

  # Display
  entries.each do |entry|
    source_file = entry[:file]
    annotation = entry[:annotation]
    blame_info = entry[:blame]

    is_mine = author_mode == "me" && git_email && blame_info &&
      blame_info[:email] == git_email

    bar_color = if file_counts[source_file] > 1
      color_code = PALETTE[Zlib.crc32(source_file.to_s) % PALETTE.size]
      "\e[38;5;#{color_code}m"
    else
      DIM
    end
    colored_bar = "#{bar_color}#{BAR}#{RESET}"
    # When highlighting "me", use bright white bar
    colored_bar = "#{WHITE}#{BAR}#{RESET}" if is_mine

    tag = annotation.tag
    tag_color = TAG_COLORS[tag] || DIM
    location = "#{source_file}:#{annotation.line}"
    colored_tag = "#{tag_color}#{BOLD}#{tag}#{RESET}"
    note = annotation.text.to_s.encode("UTF-8", invalid: :replace, undef: :replace)

    # Build the file:line row with optional author+date
    loc_line = " #{colored_bar} #{location}"

    if author_mode == "all" && blame_info
      raw_author = blame_info[:author] || ""
      author_name = author_display[raw_author] || Railbow::Params.format_author(raw_author)
      blame_date = blame_info[:date] ? blame_info[:date].strftime("%Y-%m-%d") : ""
      padding = " " * [(max_loc_width - location.length + 2), 2].max
      colored_author = "#{author_colors.color_for(raw_author)}#{author_name}#{RESET}"
      meta = "#{colored_author}  #{DIM}#{blame_date}#{RESET}"
      loc_line = " #{colored_bar} #{location}#{padding}#{meta}"
    elsif author_mode == "me" && is_mine && blame_info
      blame_date = blame_info[:date] ? blame_info[:date].strftime("%Y-%m-%d") : ""
      loc_line = " #{colored_bar} #{WHITE}#{location}#{RESET}  #{DIM}#{blame_date}#{RESET}"
    elsif author_mode != "all" && show_date && blame_info
      blame_date = blame_info[:date] ? blame_info[:date].strftime("%Y-%m-%d") : ""
      padding = " " * [(max_loc_width - location.length + 2), 2].max
      loc_line = " #{colored_bar} #{location}#{padding}#{DIM}#{blame_date}#{RESET}"
    end

    puts loc_line

    # Wrap note text so continuation lines keep the bar and align after TAG
    prefix = " #{colored_bar}   "
    tag_str = "#{colored_tag} "
    indent_width = 1 + 1 + 1 + 3 + tag.length + 1 # " " + BAR + " " + "   " + TAG + " "
    continuation_prefix = " #{colored_bar}   #{" " * (tag.length + 1)}"

    # Apply "me" highlighting to note text
    if is_mine
      tag_str = "#{WHITE}#{tag}#{RESET} "
    end

    term_w = begin
      ($stdout.tty? && $stdout.respond_to?(:winsize)) ? $stdout.winsize[1] : 120
    rescue
      120
    end
    max_note_width = [term_w - indent_width, 20].max

    if note.length <= max_note_width
      note_text = is_mine ? "#{WHITE}#{note}#{RESET}" : note
      puts "#{prefix}#{tag_str}#{note_text}"
    else
      lines = word_wrap_simple(note, max_note_width)
      first_line = is_mine ? "#{WHITE}#{lines.first}#{RESET}" : lines.first
      puts "#{prefix}#{tag_str}#{first_line}"
      lines[1..].each do |line|
        line_text = is_mine ? "#{WHITE}#{line}#{RESET}" : line
        puts "#{continuation_prefix}#{line_text}"
      end
    end
  end

  if skipped > 0
    puts
    puts "  #{DIM}#{skipped} annotation(s) older than #{since_val} hidden#{RESET}"
  end

  puts
end