Skip to content

Commit

Permalink
Adjust truncation, add opt-out mechanism, rename methods, and prepare…
Browse files Browse the repository at this point in the history
… error highlighting to render on extremely small screens
  • Loading branch information
karreiro authored and mame committed Oct 23, 2024
1 parent 0657bc1 commit c565340
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 22 deletions.
47 changes: 32 additions & 15 deletions lib/error_highlight/formatter.rb
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
module ErrorHighlight
class DefaultFormatter
MIN_SNIPPET_WIDTH = 20

def self.message_for(spot)
# currently only a one-line code snippet is supported
return "" unless spot[:first_lineno] == spot[:last_lineno]

snippet = spot[:snippet]
first_column = spot[:first_column]
last_column = spot[:last_column]
ellipsis = "..."

# truncate snippet to fit in the viewport
if snippet.size > viewport_size
visible_start = [first_column - viewport_size / 2, 0].max
visible_end = visible_start + viewport_size
if snippet_max_width && snippet.size > snippet_max_width
available_width = snippet_max_width - ellipsis.size
center = first_column - snippet_max_width / 2

# avoid centering the snippet when the error is at the end of the line
visible_start = snippet.size - viewport_size if visible_end > snippet.size
visible_start = last_column < available_width ? 0 : [center, 0].max
visible_end = visible_start + snippet_max_width
visible_start = snippet.size - snippet_max_width if visible_end > snippet.size

prefix = visible_start.positive? ? "..." : ""
suffix = visible_end < snippet.size ? "..." : ""
prefix = visible_start.positive? ? ellipsis : ""
suffix = visible_end < snippet.size ? ellipsis : ""

snippet = prefix + snippet[(visible_start + prefix.size)...(visible_end - suffix.size)] + suffix
snippet << "\n" unless snippet.end_with?("\n")

first_column = first_column - visible_start
first_column -= visible_start
last_column = [last_column - visible_start, snippet.size - 1].min
end

Expand All @@ -32,18 +36,31 @@ def self.message_for(spot)
"\n\n#{ snippet }#{ marker }"
end

def self.viewport_size
Ractor.current[:__error_highlight_viewport_size__] ||= terminal_columns
def self.snippet_max_width
return if Ractor.current[:__error_highlight_max_snippet_width__] == :disabled

Ractor.current[:__error_highlight_max_snippet_width__] ||= terminal_width
end

def self.viewport_size=(viewport_size)
Ractor.current[:__error_highlight_viewport_size__] = viewport_size
def self.snippet_max_width=(width)
return Ractor.current[:__error_highlight_max_snippet_width__] = :disabled if width.nil?

width = width.to_i

if width < MIN_SNIPPET_WIDTH
warn "'snippet_max_width' adjusted to minimum value of #{MIN_SNIPPET_WIDTH}."
width = MIN_SNIPPET_WIDTH
end

Ractor.current[:__error_highlight_max_snippet_width__] = width
end

def self.terminal_columns
# lazy load io/console, so it's not loaded when viewport_size is set
def self.terminal_width
# lazy load io/console, so it's not loaded when snippet_max_width is set
require "io/console"
IO.console.winsize[1]
STDERR.winsize[1] if STDERR.tty?
rescue LoadError, NoMethodError, SystemCallError
# do not truncate when window size is not available
end
end

Expand Down
83 changes: 76 additions & 7 deletions test/test_error_highlight.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
require "tempfile"

class ErrorHighlightTest < Test::Unit::TestCase
ErrorHighlight::DefaultFormatter.viewport_size = 80
ErrorHighlight::DefaultFormatter.snippet_max_width = 80

class DummyFormatter
def self.message_for(corrections)
Expand Down Expand Up @@ -1287,7 +1287,7 @@ def test_no_final_newline
end
end

def test_errors_on_small_viewports_at_the_end
def test_errors_on_small_terminal_window_at_the_end
assert_error_message(NoMethodError, <<~END) do
undefined method `time' for #{ ONE_RECV_MESSAGE }
Expand All @@ -1299,7 +1299,7 @@ def test_errors_on_small_viewports_at_the_end
end
end

def test_errors_on_small_viewports_at_the_beginning
def test_errors_on_small_terminal_window_at_the_beginning
assert_error_message(NoMethodError, <<~END) do
undefined method `time' for #{ ONE_RECV_MESSAGE }
Expand All @@ -1312,19 +1312,88 @@ def test_errors_on_small_viewports_at_the_beginning
end
end

def test_errors_on_small_viewports_at_the_middle
def test_errors_on_small_terminal_window_at_the_middle_near_beginning
assert_error_message(NoMethodError, <<~END) do
undefined method `time' for #{ ONE_RECV_MESSAGE }
100000000000000000000000000000000000000 + 1.time { 1000000000000000000000...
^^^^^
END

100000000000000000000000000000000000000 + 1.time { 100000000000000000000000000000000000000 }
end
end

def test_errors_on_small_terminal_window_at_the_middle
assert_error_message(NoMethodError, <<~END) do
undefined method `time' for #{ ONE_RECV_MESSAGE }
...000000000000000000000000000000000 + 1.time { 10000000000000000000000000000...
^^^^^
END

100000000000000000000000000000000000000 + 1.time { 100000000000000000000000000000000000000 }
10000000000000000000000000000000000000000000000000000000000000000000000 + 1.time { 1000000000000000000000000000000 }
end
end

def test_errors_on_extremely_small_terminal_window
custom_max_width = 30
original_max_width = ErrorHighlight::DefaultFormatter.snippet_max_width

ErrorHighlight::DefaultFormatter.snippet_max_width = custom_max_width

assert_error_message(NoMethodError, <<~END) do
undefined method `time' for #{ ONE_RECV_MESSAGE }
...00000000 + 1.time { 1000...
^^^^^
END

100000000000000 + 1.time { 100000000000000 }
end
ensure
ErrorHighlight::DefaultFormatter.snippet_max_width = original_max_width
end

def test_errors_on_terminal_window_smaller_than_min_width
custom_max_width = 5
original_max_width = ErrorHighlight::DefaultFormatter.snippet_max_width

ErrorHighlight::DefaultFormatter.snippet_max_width = custom_max_width

assert_error_message(NoMethodError, <<~END) do
undefined method `time' for #{ ONE_RECV_MESSAGE }
...000 + 1.time {...
^^^^^
END

100000000000000 + 1.time { 100000000000000 }
end
ensure
ErrorHighlight::DefaultFormatter.snippet_max_width = original_max_width
end

def test_errors_on_terminal_window_when_truncation_is_disabled
custom_max_width = nil
original_max_width = ErrorHighlight::DefaultFormatter.snippet_max_width

ErrorHighlight::DefaultFormatter.snippet_max_width = custom_max_width

assert_error_message(NoMethodError, <<~END) do
undefined method `time' for #{ ONE_RECV_MESSAGE }
10000000000000000000000000000000000000000000000000000000000000000000000 + 1.time { 1000000000000000000000000000000 }
^^^^^
END

10000000000000000000000000000000000000000000000000000000000000000000000 + 1.time { 1000000000000000000000000000000 }
end
ensure
ErrorHighlight::DefaultFormatter.snippet_max_width = original_max_width
end

def test_errors_on_small_viewports_when_larger_than_viewport
def test_errors_on_small_terminal_window_when_larger_than_viewport
assert_error_message(NoMethodError, <<~END) do
undefined method `timessssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss!' for #{ ONE_RECV_MESSAGE }
Expand All @@ -1336,7 +1405,7 @@ def test_errors_on_small_viewports_when_larger_than_viewport
end
end

def test_errors_on_small_viewports_when_exact_size_of_viewport
def test_errors_on_small_terminal_window_when_exact_size_of_viewport
assert_error_message(NoMethodError, <<~END) do
undefined method `timessssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss!' for #{ ONE_RECV_MESSAGE }
Expand Down

0 comments on commit c565340

Please sign in to comment.