mirror of
https://github.com/livebook-dev/livebook.git
synced 2024-12-27 09:53:08 +08:00
Use inspect to highlight cell result (#18)
* Use inspect to highlight cell result * Use invalid UTF-8 byte for color separation * Match editor typography in cell output
This commit is contained in:
parent
77b60c8110
commit
a8b5227dd6
5 changed files with 138 additions and 5 deletions
|
@ -116,12 +116,59 @@ iframe[hidden] {
|
||||||
@apply mb-0;
|
@apply mb-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Elixir HTML-ized inspect result */
|
||||||
|
|
||||||
|
.elixir-inspect .atom {
|
||||||
|
color: #61afef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elixir-inspect .binary {
|
||||||
|
color: #5c6370;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elixir-inspect .boolean {
|
||||||
|
color: #c678dd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elixir-inspect .list {
|
||||||
|
color: #5c6370;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elixir-inspect .map {
|
||||||
|
color: #5c6370;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elixir-inspect .nil {
|
||||||
|
color: #c678dd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elixir-inspect .number {
|
||||||
|
color: #d19a66;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elixir-inspect .regex {
|
||||||
|
color: #e06c75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elixir-inspect .string {
|
||||||
|
color: #98c379;
|
||||||
|
}
|
||||||
|
|
||||||
|
.elixir-inspect .tuple {
|
||||||
|
color: #5c6370;
|
||||||
|
}
|
||||||
|
|
||||||
/* Other */
|
/* Other */
|
||||||
|
|
||||||
.bg-editor {
|
.bg-editor {
|
||||||
background-color: #282c34;
|
background-color: #282c34;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font-editor {
|
||||||
|
font-family: "Droid Sans Mono", monospace, monospace, "Droid Sans Fallback";
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.tiny-scrollbar::-webkit-scrollbar {
|
.tiny-scrollbar::-webkit-scrollbar {
|
||||||
width: 0.4rem;
|
width: 0.4rem;
|
||||||
height: 0.4rem;
|
height: 0.4rem;
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
defmodule LiveBookWeb.Cell do
|
defmodule LiveBookWeb.Cell do
|
||||||
use LiveBookWeb, :live_component
|
use LiveBookWeb, :live_component
|
||||||
|
|
||||||
|
alias LiveBookWeb.Utils
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~L"""
|
~L"""
|
||||||
<div id="cell-<%= @cell.id %>"
|
<div id="cell-<%= @cell.id %>"
|
||||||
|
@ -131,7 +133,7 @@ defmodule LiveBookWeb.Cell do
|
||||||
assigns = %{outputs: outputs}
|
assigns = %{outputs: outputs}
|
||||||
|
|
||||||
~L"""
|
~L"""
|
||||||
<div class="flex flex-col rounded-md border border-gray-200 divide-y divide-gray-200 text-sm">
|
<div class="flex flex-col rounded-md border border-gray-200 divide-y divide-gray-200 font-editor">
|
||||||
<%= for output <- Enum.reverse(@outputs) do %>
|
<%= for output <- Enum.reverse(@outputs) do %>
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<div class="max-h-80 overflow-auto tiny-scrollbar">
|
<div class="max-h-80 overflow-auto tiny-scrollbar">
|
||||||
|
@ -152,11 +154,12 @@ defmodule LiveBookWeb.Cell do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp render_output({:ok, value}) do
|
defp render_output({:ok, value}) do
|
||||||
inspected = inspect(value, pretty: true, width: 140)
|
inspected = Utils.inspect_as_html(value, pretty: true, width: 140)
|
||||||
|
|
||||||
assigns = %{inspected: inspected}
|
assigns = %{inspected: inspected}
|
||||||
|
|
||||||
~L"""
|
~L"""
|
||||||
<div class="whitespace-pre text-gray-500"><%= @inspected %></div>
|
<div class="whitespace-pre text-gray-500 elixir-inspect"><%= @inspected %></div>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
63
lib/live_book_web/utils.ex
Normal file
63
lib/live_book_web/utils.ex
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
defmodule LiveBookWeb.Utils do
|
||||||
|
@doc """
|
||||||
|
Wraps `inspect/2` to include HTML tags in the final string for syntax highlighting.
|
||||||
|
|
||||||
|
Any options given as the second argument are passed directly to `inspect/2`.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex(2)> LiveBookWeb.Utils.inspect_as_html(:test, [])
|
||||||
|
{:safe, "<span class=\\"atom\\">:test</span>"}
|
||||||
|
"""
|
||||||
|
@spec inspect_as_html(Inspect.t(), keyword()) :: Phoenix.HTML.safe()
|
||||||
|
def inspect_as_html(term, opts \\ []) do
|
||||||
|
# Inspect coloring primary tragets terminals,
|
||||||
|
# so the colors are usually ANSI escape codes
|
||||||
|
# and their effect is reverted by a special reset escape code.
|
||||||
|
#
|
||||||
|
# In our case we need HTML tags for syntax highlighting,
|
||||||
|
# so as the colors we use sequences like \xfeatom\xfe
|
||||||
|
# then we HTML-escape the string and finally replace
|
||||||
|
# these special sequences with actual <span> tags.
|
||||||
|
#
|
||||||
|
# Note that the surrounding \xfe byte is invalid in a UTF-8 sequence,
|
||||||
|
# so we can be certain it won't appear in the normal `inspect` result.
|
||||||
|
|
||||||
|
term
|
||||||
|
|> inspect(Keyword.merge(opts, syntax_colors: inspect_html_colors()))
|
||||||
|
|> Phoenix.HTML.html_escape()
|
||||||
|
|> Phoenix.HTML.safe_to_string()
|
||||||
|
|> replace_colors_with_tags()
|
||||||
|
|> Phoenix.HTML.raw()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp inspect_html_colors() do
|
||||||
|
delim = "\xfe"
|
||||||
|
|
||||||
|
[
|
||||||
|
atom: delim <> "atom" <> delim,
|
||||||
|
binary: delim <> "binary" <> delim,
|
||||||
|
boolean: delim <> "boolean" <> delim,
|
||||||
|
list: delim <> "list" <> delim,
|
||||||
|
map: delim <> "map" <> delim,
|
||||||
|
number: delim <> "number" <> delim,
|
||||||
|
nil: delim <> "nil" <> delim,
|
||||||
|
regex: delim <> "regex" <> delim,
|
||||||
|
string: delim <> "string" <> delim,
|
||||||
|
tuple: delim <> "tuple" <> delim,
|
||||||
|
reset: delim <> "reset" <> delim
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp replace_colors_with_tags(string) do
|
||||||
|
colors = inspect_html_colors()
|
||||||
|
|
||||||
|
Enum.reduce(colors, string, fn
|
||||||
|
{:reset, color}, string ->
|
||||||
|
String.replace(string, color, "</span>")
|
||||||
|
|
||||||
|
{key, color}, string ->
|
||||||
|
String.replace(string, color, "<span class=\"#{Atom.to_string(key)}\">")
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
|
@ -92,7 +92,8 @@ defmodule LiveBookWeb.SessionLiveTest do
|
||||||
Session.get_data(session_id)
|
Session.get_data(session_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "queueing cell evaluation updates the shared state", %{conn: conn, session_id: session_id} do
|
test "queueing cell evaluation updates the shared state",
|
||||||
|
%{conn: conn, session_id: session_id} do
|
||||||
section_id = insert_section(session_id)
|
section_id = insert_section(session_id)
|
||||||
cell_id = insert_cell(session_id, section_id, :elixir, "Process.sleep(5)")
|
cell_id = insert_cell(session_id, section_id, :elixir, "Process.sleep(5)")
|
||||||
|
|
||||||
|
@ -108,7 +109,6 @@ defmodule LiveBookWeb.SessionLiveTest do
|
||||||
|
|
||||||
test "queueing cell evaluation defaults to the focused cell if no cell id is given",
|
test "queueing cell evaluation defaults to the focused cell if no cell id is given",
|
||||||
%{conn: conn, session_id: session_id} do
|
%{conn: conn, session_id: session_id} do
|
||||||
|
|
||||||
section_id = insert_section(session_id)
|
section_id = insert_section(session_id)
|
||||||
cell_id = insert_cell(session_id, section_id, :elixir, "Process.sleep(5)")
|
cell_id = insert_cell(session_id, section_id, :elixir, "Process.sleep(5)")
|
||||||
|
|
||||||
|
|
20
test/live_book_web/utils_test.exs
Normal file
20
test/live_book_web/utils_test.exs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
defmodule LiveBookWeb.UtilsTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias LiveBookWeb.Utils
|
||||||
|
|
||||||
|
doctest Utils
|
||||||
|
|
||||||
|
describe "inspect_as_html/2" do
|
||||||
|
test "uses span tags for term highlighting" do
|
||||||
|
assert {:safe,
|
||||||
|
~s{<span class="list">[</span><span class="number">1</span><span class="list">,</span> <span class="number">2</span><span class="list">]</span>}} ==
|
||||||
|
Utils.inspect_as_html([1, 2])
|
||||||
|
end
|
||||||
|
|
||||||
|
test "escapes HTML in the inspect result" do
|
||||||
|
assert {:safe, ~s{<span class="string">"1 < 2"</span>}} ==
|
||||||
|
Utils.inspect_as_html("1 < 2")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue