Improve errors formatting (#936)

* Use monospaced font and wrapping for errors

* Improve function clause error format
This commit is contained in:
Jonatan Kłosko 2022-01-25 21:55:24 +01:00 committed by GitHub
parent 6b19f1d71b
commit 00c2cfb31a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 65 additions and 23 deletions

View file

@ -349,24 +349,18 @@ defmodule Livebook.Evaluator do
defp eval(code, binding, env) do defp eval(code, binding, env) do
try do try do
quoted = Code.string_to_quoted!(code) quoted = Code.string_to_quoted!(code, file: env.file)
# TODO: Use Code.eval_quoted_with_env/3 on Elixir v1.14 # TODO: Use Code.eval_quoted_with_env/3 on Elixir v1.14
{result, binding, env} = :elixir.eval_quoted(quoted, binding, env) {result, binding, env} = :elixir.eval_quoted(quoted, binding, env)
{:ok, result, binding, env} {:ok, result, binding, env}
catch catch
kind, error -> kind, error ->
{kind, error, stacktrace} = prepare_error(kind, error, __STACKTRACE__) stacktrace = prune_stacktrace(__STACKTRACE__)
{:error, kind, error, stacktrace} {:error, kind, error, stacktrace}
end end
end end
defp prepare_error(kind, error, stacktrace) do
{error, stacktrace} = Exception.blame(kind, error, stacktrace)
stacktrace = prune_stacktrace(stacktrace)
{kind, error, stacktrace}
end
# Adapted from https://github.com/elixir-lang/elixir/blob/1c1654c88adfdbef38ff07fc30f6fbd34a542c07/lib/iex/lib/iex/evaluator.ex#L355-L372 # Adapted from https://github.com/elixir-lang/elixir/blob/1c1654c88adfdbef38ff07fc30f6fbd34a542c07/lib/iex/lib/iex/evaluator.ex#L355-L372
@elixir_internals [:elixir, :elixir_expand, :elixir_compiler, :elixir_module] ++ @elixir_internals [:elixir, :elixir_expand, :elixir_compiler, :elixir_module] ++

View file

@ -26,7 +26,7 @@ defmodule Livebook.Evaluator.DefaultFormatter do
end end
def format_response({:error, kind, error, stacktrace}) do def format_response({:error, kind, error, stacktrace}) do
formatted = Exception.format(kind, error, stacktrace) formatted = format_error(kind, error, stacktrace)
{:error, formatted, error_type(error)} {:error, formatted, error_type(error)}
end end
@ -61,12 +61,7 @@ defmodule Livebook.Evaluator.DefaultFormatter do
end end
end end
defp format_error(kind, error, stacktrace) do defp inspect_opts(opts \\ []) do
{error, stacktrace} = Exception.blame(kind, error, stacktrace)
Exception.format(kind, error, stacktrace)
end
defp inspect_opts(opts) do
default_opts = [pretty: true, width: 100, syntax_colors: syntax_colors()] default_opts = [pretty: true, width: 100, syntax_colors: syntax_colors()]
Keyword.merge(default_opts, opts) Keyword.merge(default_opts, opts)
end end
@ -103,4 +98,47 @@ defmodule Livebook.Evaluator.DefaultFormatter do
Exception.message(exception) =~ Exception.message(exception) =~
"Mix.install/2 can only be called with the same dependencies" "Mix.install/2 can only be called with the same dependencies"
end end
defp format_error(kind, error, stacktrace) do
{blamed, stacktrace} = Exception.blame(kind, error, stacktrace)
banner =
case blamed do
%FunctionClauseError{} ->
banner = Exception.format_banner(kind, error, stacktrace)
blame = FunctionClauseError.blame(blamed, &inspect(&1, inspect_opts()), &blame_match/1)
[error_color(banner), pad(blame)]
_ ->
banner = Exception.format_banner(kind, blamed, stacktrace)
error_color(banner)
end
message =
if stacktrace == [] do
banner
else
stacktrace = Exception.format_stacktrace(stacktrace)
[banner, "\n", error_color(stacktrace)]
end
IO.iodata_to_binary(message)
end
defp blame_match(%{match?: true, node: node}), do: Macro.to_string(node)
defp blame_match(%{match?: false, node: node}) do
node
|> Macro.to_string()
|> error_color()
|> IO.iodata_to_binary()
end
defp pad(string) do
" " <> String.replace(string, "\n", "\n ")
end
defp error_color(string) do
IO.ANSI.format([:red, string], true)
end
end end

View file

@ -1,6 +1,8 @@
defmodule LivebookWeb.Output do defmodule LivebookWeb.Output do
use Phoenix.Component use Phoenix.Component
import LivebookWeb.Helpers
alias LivebookWeb.Output alias LivebookWeb.Output
@doc """ @doc """
@ -93,7 +95,7 @@ defmodule LivebookWeb.Output do
~H""" ~H"""
<div class="flex flex-col space-y-4"> <div class="flex flex-col space-y-4">
<%= render_error_message_output(@formatted) %> <%= render_error(@formatted) %>
<%= if @is_standalone do %> <%= if @is_standalone do %>
<div> <div>
<button class="button-base button-gray" phx-click="restart_runtime"> <button class="button-base button-gray" phx-click="restart_runtime">
@ -112,7 +114,7 @@ defmodule LivebookWeb.Output do
end end
defp render_output({:error, formatted, _type}, %{}) do defp render_output({:error, formatted, _type}, %{}) do
render_error_message_output(formatted) render_error(formatted)
end end
# TODO: remove on Livebook v0.7 # TODO: remove on Livebook v0.7
@ -123,24 +125,32 @@ defmodule LivebookWeb.Output do
:table_dynamic, :table_dynamic,
:frame_dynamic :frame_dynamic
] do ] do
render_error_message_output(""" render_error_message("""
Legacy output format: #{inspect(output)}. Please update Kino to Legacy output format: #{inspect(output)}. Please update Kino to
the latest version. the latest version.
""") """)
end end
defp render_output(output, %{}) do defp render_output(output, %{}) do
render_error_message_output(""" render_error_message("""
Unknown output format: #{inspect(output)}. If you're using Kino, Unknown output format: #{inspect(output)}. If you're using Kino,
you may want to update Kino and Livebook to the latest version. you may want to update Kino and Livebook to the latest version.
""") """)
end end
defp render_error_message_output(message) do defp render_error(message) do
assigns = %{message: message} assigns = %{message: message}
~H""" ~H"""
<div class="overflow-auto whitespace-pre text-red-600 tiny-scrollbar"><%= @message %></div> <div class="whitespace-pre-wrap font-editor text-gray-500"><%= ansi_string_to_html(@message) %></div>
"""
end
defp render_error_message(message) do
assigns = %{message: message}
~H"""
<div class="whitespace-pre-wrap font-editor text-red-600"><%= @message %></div>
""" """
end end
end end

View file

@ -94,8 +94,8 @@ defmodule Livebook.EvaluatorTest do
Evaluator.evaluate_code(evaluator, self(), code, :code_1) Evaluator.evaluate_code(evaluator, self(), code, :code_1)
assert_receive {:evaluation_response, :code_1, assert_receive {:evaluation_response, :code_1,
{:error, :error, %FunctionClauseError{}, {:error, :error, :function_clause, [{List, :first, _arity, _location}]},
[{List, :first, _arity, _location}]}, metadata()} metadata()}
end end
test "in case of an error returns only the relevant part of stacktrace", test "in case of an error returns only the relevant part of stacktrace",