|
| 1 | +# rbs_inline: enabled |
| 2 | +# frozen_string_literal: true |
| 3 | + |
| 4 | +module Lrama |
| 5 | + module Diagnostics |
| 6 | + class Formatter |
| 7 | + GUTTER_WIDTH = 5 |
| 8 | + GUTTER_SEPARATOR = ' | ' |
| 9 | + |
| 10 | + # @rbs (?color_enabled: bool, ?show_source: bool, ?show_caret: bool) -> void |
| 11 | + def initialize(color_enabled: false, show_source: true, show_caret: true) |
| 12 | + @color_enabled = color_enabled |
| 13 | + @show_source = show_source |
| 14 | + @show_caret = show_caret |
| 15 | + end |
| 16 | + |
| 17 | + # @rbs (Message message) -> String |
| 18 | + def format(message) |
| 19 | + lines = [] #: Array[String] |
| 20 | + |
| 21 | + lines << format_main_line(message) |
| 22 | + |
| 23 | + if @show_source && message.source_line? |
| 24 | + lines << format_source_line(message) |
| 25 | + |
| 26 | + if @show_caret |
| 27 | + lines << format_caret_line(message) |
| 28 | + end |
| 29 | + |
| 30 | + if message.fixit? |
| 31 | + lines << format_fixit_line(message) |
| 32 | + end |
| 33 | + end |
| 34 | + |
| 35 | + message.notes.each do |note| |
| 36 | + lines << format_note(note) |
| 37 | + end |
| 38 | + |
| 39 | + lines.join("\n") |
| 40 | + end |
| 41 | + |
| 42 | + # @rbs (Array[Message] messages) -> String |
| 43 | + def format_all(messages) |
| 44 | + messages.map { |m| format(m) }.join("\n\n") |
| 45 | + end |
| 46 | + |
| 47 | + private |
| 48 | + |
| 49 | + # @rbs (Message message) -> String |
| 50 | + def format_main_line(message) |
| 51 | + parts = [] #: Array[String] |
| 52 | + |
| 53 | + if message.location? |
| 54 | + parts << format_location(message) |
| 55 | + parts << ': ' |
| 56 | + end |
| 57 | + |
| 58 | + parts << colorize(message.type.to_s, message.type) |
| 59 | + parts << ': ' |
| 60 | + parts << format_message_text(message.message) |
| 61 | + |
| 62 | + parts.join |
| 63 | + end |
| 64 | + |
| 65 | + # @rbs (Message message) -> String |
| 66 | + def format_location(message) |
| 67 | + return '' unless message.location? |
| 68 | + |
| 69 | + str = "#{message.file}:#{message.line}" |
| 70 | + |
| 71 | + if message.line == message.end_line |
| 72 | + if message.column == message.end_column |
| 73 | + str += ".#{message.column}" |
| 74 | + else |
| 75 | + str += ".#{message.column}-#{message.end_column}" |
| 76 | + end |
| 77 | + else |
| 78 | + str += ".#{message.column}-#{message.end_line}.#{message.end_column}" |
| 79 | + end |
| 80 | + |
| 81 | + colorize(str, :location) |
| 82 | + end |
| 83 | + |
| 84 | + # @rbs (String text) -> String |
| 85 | + def format_message_text(text) |
| 86 | + text.gsub(/'([^']+)'/) do |_match| |
| 87 | + quoted = $1 || '' |
| 88 | + "'" + colorize(quoted, :quote) + "'" |
| 89 | + end |
| 90 | + end |
| 91 | + |
| 92 | + # @rbs (Message message) -> String |
| 93 | + def format_source_line(message) |
| 94 | + line_num = message.line.to_s.rjust(GUTTER_WIDTH) |
| 95 | + gutter = "#{line_num}#{GUTTER_SEPARATOR}" |
| 96 | + source = highlight_source(message) |
| 97 | + |
| 98 | + "#{gutter}#{source}" |
| 99 | + end |
| 100 | + |
| 101 | + # @rbs (Message message) -> String |
| 102 | + def highlight_source(message) |
| 103 | + source = message.source_line || '' |
| 104 | + return source unless @color_enabled && message.location? |
| 105 | + |
| 106 | + col = (message.column || 1) - 1 |
| 107 | + end_col = (message.end_column || message.column || 1) - 1 |
| 108 | + |
| 109 | + return source if col < 0 || col >= source.length |
| 110 | + end_col = [end_col, source.length].min |
| 111 | + |
| 112 | + before = source[0...col] || '' |
| 113 | + highlight = source[col...end_col] || '' |
| 114 | + after = source[end_col..] || '' |
| 115 | + |
| 116 | + "#{before}#{colorize(highlight, :unexpected)}#{after}" |
| 117 | + end |
| 118 | + |
| 119 | + # @rbs (Message message) -> String |
| 120 | + def format_caret_line(message) |
| 121 | + gutter = ' ' * GUTTER_WIDTH + GUTTER_SEPARATOR |
| 122 | + padding = leading_whitespace(message) |
| 123 | + caret = build_caret(message) |
| 124 | + |
| 125 | + "#{gutter}#{padding}#{colorize(caret, :caret)}" |
| 126 | + end |
| 127 | + |
| 128 | + # @rbs (Message message) -> String |
| 129 | + def leading_whitespace(message) |
| 130 | + source = message.source_line || '' |
| 131 | + col = message.column || 0 |
| 132 | + return '' if col <= 0 |
| 133 | + |
| 134 | + prefix = source[0...col] || '' |
| 135 | + prefix.gsub(/[^\t]/, ' ') |
| 136 | + end |
| 137 | + |
| 138 | + # @rbs (Message message) -> String |
| 139 | + def build_caret(message) |
| 140 | + length = message.range_length |
| 141 | + |
| 142 | + if length <= 1 |
| 143 | + '^' |
| 144 | + else |
| 145 | + '^' + '~' * (length - 1) |
| 146 | + end |
| 147 | + end |
| 148 | + |
| 149 | + # @rbs (Message message) -> String |
| 150 | + def format_fixit_line(message) |
| 151 | + gutter = ' ' * GUTTER_WIDTH + GUTTER_SEPARATOR |
| 152 | + padding = ' ' * [(message.column || 1) - 1, 0].max |
| 153 | + fixit_text = colorize(message.fixit || '', :fixit_insert) |
| 154 | + |
| 155 | + "#{gutter}#{padding}#{fixit_text}" |
| 156 | + end |
| 157 | + |
| 158 | + # @rbs (Message note) -> String |
| 159 | + def format_note(note) |
| 160 | + parts = [] #: Array[String] |
| 161 | + |
| 162 | + if note.location? |
| 163 | + parts << format_location(note) |
| 164 | + parts << ': ' |
| 165 | + end |
| 166 | + |
| 167 | + parts << colorize('note', :note) |
| 168 | + parts << ': ' |
| 169 | + parts << note.message |
| 170 | + |
| 171 | + parts.join |
| 172 | + end |
| 173 | + |
| 174 | + # @rbs (String? text, Symbol style) -> String |
| 175 | + def colorize(text, style) |
| 176 | + return text || '' unless @color_enabled |
| 177 | + |
| 178 | + Color.colorize(text || '', style) |
| 179 | + end |
| 180 | + end |
| 181 | + end |
| 182 | +end |
0 commit comments