Virtualize output lines (#70)

* Virtualize output lines

* Remove unused dependency

* Remove VirtualizedLinesComponent

* Pass lines as HTML nodes and use as template

* Validate hook children

* Refactor markup
This commit is contained in:
Jonatan Kłosko 2021-03-04 22:09:57 +01:00 committed by GitHub
parent 199602016c
commit e65a5f712c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 15787 additions and 26 deletions

View file

@ -19,7 +19,7 @@ to be consistent with the editor.
--ansi-color-light-green: #34d399;
--ansi-color-light-yellow: #fde68a;
--ansi-color-light-blue: #93c5fd;
--ansi-color-light-magenta: #F472b6;
--ansi-color-light-magenta: #f472b6;
--ansi-color-light-cyan: #6be3f2;
--ansi-color-light-white: white;
}

View file

@ -9,6 +9,7 @@ import Cell from "./cell";
import Session from "./session";
import FocusOnUpdate from "./focus_on_update";
import ScrollOnUpdate from "./scroll_on_update";
import VirtualizedLines from "./virtualized_lines";
const Hooks = {
ContentEditable,
@ -16,6 +17,7 @@ const Hooks = {
Session,
FocusOnUpdate,
ScrollOnUpdate,
VirtualizedLines,
};
const csrfToken = document

View file

@ -0,0 +1,79 @@
import HyperList from "hyperlist";
import { getAttributeOrThrow, parseInteger } from "../lib/attribute";
/**
* A hook used to render text lines as a virtual list,
* so that only the visible lines are actually in the DOM.
*
* Configuration:
*
* * `data-max-height` - the maximum height of the element, exceeding this height enables scrolling
*
* The element should have two children:
*
* * one annotated with `data-template` attribute, it should be hidden
* and contain all the line elements as its children
*
* * one annotated with `data-content` where the visible elements are rendered,
* it should contain any styling relevant for the container
*/
const VirtualizedLines = {
mounted() {
this.props = getProps(this);
const computedStyle = window.getComputedStyle(this.el);
this.lineHeight = parseInt(computedStyle.lineHeight, 10);
this.templateElement = this.el.querySelector('[data-template]');
if (!this.templateElement) {
throw new Error('VirtualizedLines must have a child with data-template attribute');
}
this.contentElement = this.el.querySelector('[data-content]');
if (!this.templateElement) {
throw new Error('VirtualizedLines must have a child with data-content');
}
const config = hyperListConfig(
this.templateElement,
this.props.maxHeight,
this.lineHeight
);
this.virtualizedList = new HyperList(this.contentElement, config);
},
updated() {
this.props = getProps(this);
const config = hyperListConfig(
this.templateElement,
this.props.maxHeight,
this.lineHeight
);
this.virtualizedList.refresh(this.contentElement, config);
},
};
function hyperListConfig(templateElement, maxHeight, lineHeight) {
const numberOfLines = templateElement.childElementCount;
return {
height: Math.min(maxHeight, lineHeight * numberOfLines),
total: numberOfLines,
itemHeight: lineHeight,
generate: (index) => {
// Clone n-th child of the template container.
return templateElement.children.item(index).cloneNode(true);
},
};
}
function getProps(hook) {
return {
maxHeight: getAttributeOrThrow(hook.el, "data-max-height", parseInteger),
};
}
export default VirtualizedLines;

15686
assets/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -11,6 +11,7 @@
},
"dependencies": {
"dompurify": "^2.2.6",
"hyperlist": "^1.0.0",
"marked": "^1.2.8",
"monaco-editor": "^0.21.2",
"morphdom": "^2.6.1",

View file

@ -86,7 +86,7 @@ defmodule LivebookWeb.CellComponent do
<%= if @cell.outputs != [] do %>
<div class="mt-2">
<%= render_outputs(@cell.outputs) %>
<%= render_outputs(@cell.outputs, @cell.id) %>
</div>
<% end %>
"""
@ -162,15 +162,15 @@ defmodule LivebookWeb.CellComponent do
"""
end
defp render_outputs(outputs) do
assigns = %{outputs: outputs}
defp render_outputs(outputs, cell_id) do
assigns = %{outputs: outputs, cell_id: cell_id}
~L"""
<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, index} <- @outputs |> Enum.reverse() |> Enum.with_index() do %>
<div class="p-4">
<div class="max-h-80 overflow-auto tiny-scrollbar">
<%= render_output(output) %>
<div class="">
<%= render_output(output, "#{@cell_id}-output#{index}") %>
</div>
</div>
<% end %>
@ -178,25 +178,31 @@ defmodule LivebookWeb.CellComponent do
"""
end
defp render_output(output) when is_binary(output) do
output_html = ansi_string_to_html(output)
assigns = %{output_html: output_html}
defp render_output(output, id) when is_binary(output) do
lines = ansi_to_html_lines(output)
assigns = %{lines: lines, id: id}
~L"""
<div class="whitespace-pre text-gray-500"><%= @output_html %></div>
<div id="<%= @id %>" phx-hook="VirtualizedLines" data-max-height="300">
<div data-template class="hidden"><%= for line <- @lines do %><div><%= raw line %></div><% end %></div>
<div data-content phx-update="ignore" class="overflow-auto whitespace-pre text-gray-500 tiny-scrollbar"></div>
</div>
"""
end
defp render_output({:inspect, inspected}) do
inspected_html = ansi_string_to_html(inspected)
assigns = %{inspected_html: inspected_html}
defp render_output({:inspect, inspected}, id) do
lines = ansi_to_html_lines(inspected)
assigns = %{lines: lines, id: id}
~L"""
<div class="whitespace-pre text-gray-500"><%= @inspected_html %></div>
<div id="<%= @id %>" phx-hook="VirtualizedLines" data-max-height="300">
<div data-template class="hidden"><%= for line <- @lines do %><div><%= raw line %></div><% end %></div>
<div data-content phx-update="ignore" class="overflow-auto whitespace-pre text-gray-500 tiny-scrollbar"></div>
</div>
"""
end
defp render_output({:error, formatted}) do
defp render_output({:error, formatted}, _id) do
assigns = %{formatted: formatted}
~L"""
@ -204,6 +210,13 @@ defmodule LivebookWeb.CellComponent do
"""
end
defp ansi_to_html_lines(string) do
string
|> ansi_string_to_html()
|> Phoenix.HTML.safe_to_string()
|> String.split("\n")
end
defp render_cell_status(%{evaluation_status: :evaluating}) do
assigns = %{}