diff --git a/assets/js/hooks/markdown_renderer.js b/assets/js/hooks/markdown_renderer.js index a62a6d295..bfc008bf1 100644 --- a/assets/js/hooks/markdown_renderer.js +++ b/assets/js/hooks/markdown_renderer.js @@ -1,35 +1,47 @@ import { getAttributeOrThrow } from "../lib/attribute"; import Markdown from "../lib/markdown"; +import { findChildOrThrow } from "../lib/utils"; /** * A hook used to render Markdown content on the client. * * ## Configuration * - * * `data-id` - id of the renderer, under which the content event - * is pushed + * * `data-base-path` - the path to resolve relative URLs against + * + * * `data-allowed-uri-schemes` - a comma separated list of additional + * URI schemes that should be kept during sanitization + * + * The element should have two children: + * + * * `[data-template]` - a hidden container containing the markdown + * content. The DOM structure is ignored, only text content matters + * + * * `[data-content]` - the target element to render results into + * */ const MarkdownRenderer = { mounted() { this.props = this.getProps(); - const markdown = new Markdown(this.el, "", { - baseUrl: this.props.sessionPath, + this.templateEl = findChildOrThrow(this.el, "[data-template]"); + this.contentEl = findChildOrThrow(this.el, "[data-content]"); + + this.markdown = new Markdown(this.contentEl, this.templateEl.textContent, { + baseUrl: this.props.basePath, allowedUriSchemes: this.props.allowedUriSchemes.split(","), }); + }, - this.handleEvent( - `markdown_renderer:${this.props.id}:content`, - ({ content }) => { - markdown.setContent(content); - } - ); + updated() { + this.props = this.getProps(); + + this.markdown.setContent(this.templateEl.textContent); }, getProps() { return { - id: getAttributeOrThrow(this.el, "data-id"), - sessionPath: getAttributeOrThrow(this.el, "data-session-path"), + basePath: getAttributeOrThrow(this.el, "data-base-path"), allowedUriSchemes: getAttributeOrThrow( this.el, "data-allowed-uri-schemes" diff --git a/lib/livebook/live_markdown/export.ex b/lib/livebook/live_markdown/export.ex index 676b1f69c..94ace9690 100644 --- a/lib/livebook/live_markdown/export.ex +++ b/lib/livebook/live_markdown/export.ex @@ -212,7 +212,7 @@ defmodule Livebook.LiveMarkdown.Export do |> Enum.intersperse("\n\n") end - defp render_output({:stdout, text}, _ctx) do + defp render_output({:terminal_text, text, %{}}, _ctx) do text = String.replace_suffix(text, "\n", "") delimiter = MarkdownHelpers.code_block_delimiter(text) text = strip_ansi(text) @@ -221,14 +221,6 @@ defmodule Livebook.LiveMarkdown.Export do |> prepend_metadata(%{output: true}) end - defp render_output({:text, text}, _ctx) do - delimiter = MarkdownHelpers.code_block_delimiter(text) - text = strip_ansi(text) - - [delimiter, "\n", text, "\n", delimiter] - |> prepend_metadata(%{output: true}) - end - defp render_output( {:js, %{export: %{info_string: info_string, key: key}, js_view: %{ref: ref}}}, ctx diff --git a/lib/livebook/live_markdown/import.ex b/lib/livebook/live_markdown/import.ex index 18f9e44a3..a5b89cb2f 100644 --- a/lib/livebook/live_markdown/import.ex +++ b/lib/livebook/live_markdown/import.ex @@ -195,7 +195,7 @@ defmodule Livebook.LiveMarkdown.Import do [{"pre", _, [{"code", [{"class", "output"}], [output], %{}}], %{}} | ast], outputs ) do - take_outputs(ast, [{:text, output} | outputs]) + take_outputs(ast, [{:terminal_text, output, %{chunk: false}} | outputs]) end defp take_outputs( @@ -206,7 +206,7 @@ defmodule Livebook.LiveMarkdown.Import do ], outputs ) do - take_outputs(ast, [{:text, output} | outputs]) + take_outputs(ast, [{:terminal_text, output, %{chunk: false}} | outputs]) end # Ignore other exported outputs diff --git a/lib/livebook/notebook.ex b/lib/livebook/notebook.ex index 634dd3ffb..992c932f0 100644 --- a/lib/livebook/notebook.ex +++ b/lib/livebook/notebook.ex @@ -666,9 +666,21 @@ defmodule Livebook.Notebook do @doc """ Adds new output to the given cell. - Automatically merges stdout outputs and updates frames. + Automatically merges terminal outputs and updates frames. """ @spec add_cell_output(t(), Cell.id(), Livebook.Runtime.output()) :: t() + def add_cell_output(notebook, cell_id, output) + + # Map legacy outputs + def add_cell_output(notebook, cell_id, {:text, text}), + do: add_cell_output(notebook, cell_id, {:terminal_text, text, %{chunk: false}}) + + def add_cell_output(notebook, cell_id, {:plain_text, text}), + do: add_cell_output(notebook, cell_id, {:plain_text, text, %{chunk: false}}) + + def add_cell_output(notebook, cell_id, {:markdown, text}), + do: add_cell_output(notebook, cell_id, {:markdown, text, %{chunk: false}}) + def add_cell_output(notebook, cell_id, output) do {notebook, counter} = do_add_cell_output(notebook, cell_id, notebook.output_counter, output) %{notebook | output_counter: counter} @@ -714,45 +726,80 @@ defmodule Livebook.Notebook do end) end - defp apply_frame_update(_outputs, new_outputs, :replace), do: new_outputs - defp apply_frame_update(outputs, new_outputs, :append), do: new_outputs ++ outputs + defp apply_frame_update(_outputs, new_outputs, :replace) do + merge_chunk_outputs(new_outputs) + end + + defp apply_frame_update(outputs, new_outputs, :append) do + Enum.reduce(Enum.reverse(new_outputs), outputs, &add_output(&2, &1)) + end defp add_output(outputs, {_idx, :ignored}), do: outputs - defp add_output([], {idx, {:stdout, text}}), - do: [{idx, {:stdout, normalize_stdout(text)}}] - - defp add_output([], output), do: [output] - - # Session clients prune stdout content and handle subsequent - # ones by directly appending page content to the previous one - defp add_output([{_idx1, {:stdout, :__pruned__}} | _] = outputs, {_idx2, {:stdout, _text}}) do - outputs + # Session clients prune rendered chunks, we only keep add the new one + defp add_output( + [{idx, {type, :__pruned__, %{chunk: true} = info}} | tail], + {_idx, {type, text, %{chunk: true}}} + ) + when type in [:terminal_text, :plain_text, :markdown] do + [{idx, {type, text, info}} | tail] end - # Session server keeps all outputs, so we merge consecutive stdouts - defp add_output([{idx, {:stdout, text}} | tail], {_idx, {:stdout, cont}}) do - [{idx, {:stdout, normalize_stdout(text <> cont)}} | tail] + # Session server keeps all outputs, so we merge consecutive chunks + defp add_output( + [{idx, {:terminal_text, text, %{chunk: true} = info}} | tail], + {_idx, {:terminal_text, cont, %{chunk: true}}} + ) do + [{idx, {:terminal_text, normalize_terminal_text(text <> cont), info}} | tail] + end + + defp add_output(outputs, {idx, {:terminal_text, text, info}}) do + [{idx, {:terminal_text, normalize_terminal_text(text), info}} | outputs] + end + + defp add_output( + [{idx, {type, text, %{chunk: true} = info}} | tail], + {_idx, {type, cont, %{chunk: true}}} + ) + when type in [:plain_text, :markdown] do + [{idx, {type, normalize_terminal_text(text <> cont), info}} | tail] + end + + defp add_output(outputs, {idx, {type, text, info}}) when type in [:plain_text, :markdown] do + [{idx, {type, normalize_terminal_text(text), info}} | outputs] + end + + defp add_output(outputs, {idx, {type, container_outputs, info}}) when type in [:frame, :grid] do + [{idx, {type, merge_chunk_outputs(container_outputs), info}} | outputs] end defp add_output(outputs, output), do: [output | outputs] - @doc """ - Normalizes a text chunk coming form the standard output. - - Handles CR rawinds and caps output lines. - """ - @spec normalize_stdout(String.t()) :: String.t() - def normalize_stdout(text) do - text - |> Livebook.Utils.apply_rewind() - |> Livebook.Utils.cap_lines(max_stdout_lines()) + defp merge_chunk_outputs(outputs) do + outputs + |> Enum.reverse() + |> Enum.reduce([], &add_output(&2, &1)) end @doc """ - The maximum desired number of lines of the standard output. + Normalizes terminal text chunk. + + Handles CR rewinds and caps output lines. """ - def max_stdout_lines(), do: 1_000 + @spec normalize_terminal_text(String.t()) :: String.t() + def normalize_terminal_text(text) do + text + |> Livebook.Utils.apply_rewind() + |> Livebook.Utils.cap_lines(max_terminal_lines()) + end + + @doc """ + The maximum desired number of lines of terminal text. + + This is particularly relevant for standard output, which may receive + a lot of lines. + """ + def max_terminal_lines(), do: 1_000 @doc """ Recursively adds index to all outputs, including frames. @@ -803,60 +850,65 @@ defmodule Livebook.Notebook do @spec prune_cell_outputs(t()) :: t() def prune_cell_outputs(notebook) do update_cells(notebook, fn - %{outputs: _outputs} = cell -> %{cell | outputs: prune_outputs(cell.outputs)} + %{outputs: _outputs} = cell -> %{cell | outputs: prune_outputs(cell.outputs, true)} cell -> cell end) end - defp prune_outputs(outputs) do + defp prune_outputs(outputs, appendable?) do outputs |> Enum.reverse() - |> do_prune_outputs([]) + |> do_prune_outputs(appendable?, []) end - defp do_prune_outputs([], acc), do: acc + defp do_prune_outputs([], _appendable?, acc), do: acc - # Keep the last stdout, so that we know to message it directly, but remove its contents - defp do_prune_outputs([{idx, {:stdout, _}}], acc) do - [{idx, {:stdout, :__pruned__}} | acc] + # Keep trailing outputs that can be merged with subsequent outputs + defp do_prune_outputs([{idx, {type, _, %{chunk: true} = info}}], true = _appendable?, acc) + when type in [:terminal_text, :plain_text, :markdown] do + [{idx, {type, :__pruned__, info}} | acc] end # Keep frame and its relevant contents - defp do_prune_outputs([{idx, {:frame, frame_outputs, info}} | outputs], acc) do - do_prune_outputs(outputs, [{idx, {:frame, prune_outputs(frame_outputs), info}} | acc]) + defp do_prune_outputs([{idx, {:frame, frame_outputs, info}} | outputs], appendable?, acc) do + do_prune_outputs( + outputs, + appendable?, + [{idx, {:frame, prune_outputs(frame_outputs, true), info}} | acc] + ) end # Keep layout output and its relevant contents - defp do_prune_outputs([{idx, {:tabs, tabs_outputs, info}} | outputs], acc) do - case prune_outputs(tabs_outputs) do + defp do_prune_outputs([{idx, {:tabs, tabs_outputs, info}} | outputs], appendable?, acc) do + case prune_outputs(tabs_outputs, false) do [] -> - do_prune_outputs(outputs, acc) + do_prune_outputs(outputs, appendable?, acc) pruned_tabs_outputs -> info = Map.replace(info, :labels, :__pruned__) - do_prune_outputs(outputs, [{idx, {:tabs, pruned_tabs_outputs, info}} | acc]) + do_prune_outputs(outputs, appendable?, [{idx, {:tabs, pruned_tabs_outputs, info}} | acc]) end end - defp do_prune_outputs([{idx, {:grid, grid_outputs, info}} | outputs], acc) do - case prune_outputs(grid_outputs) do + defp do_prune_outputs([{idx, {:grid, grid_outputs, info}} | outputs], appendable?, acc) do + case prune_outputs(grid_outputs, false) do [] -> - do_prune_outputs(outputs, acc) + do_prune_outputs(outputs, appendable?, acc) pruned_grid_outputs -> - do_prune_outputs(outputs, [{idx, {:grid, pruned_grid_outputs, info}} | acc]) + do_prune_outputs(outputs, appendable?, [{idx, {:grid, pruned_grid_outputs, info}} | acc]) end end # Keep outputs that get re-rendered - defp do_prune_outputs([{idx, output} | outputs], acc) + defp do_prune_outputs([{idx, output} | outputs], appendable?, acc) when elem(output, 0) in [:input, :control, :error] do - do_prune_outputs(outputs, [{idx, output} | acc]) + do_prune_outputs(outputs, appendable?, [{idx, output} | acc]) end # Remove everything else - defp do_prune_outputs([_output | outputs], acc) do - do_prune_outputs(outputs, acc) + defp do_prune_outputs([_output | outputs], appendable?, acc) do + do_prune_outputs(outputs, appendable?, acc) end @doc """ diff --git a/lib/livebook/runtime.ex b/lib/livebook/runtime.ex index a3e18d8b3..00bc0945f 100644 --- a/lib/livebook/runtime.ex +++ b/lib/livebook/runtime.ex @@ -72,14 +72,12 @@ defprotocol Livebook.Runtime do """ @type output :: :ignored - # IO output, adjacent such outputs are treated as a whole - | {:stdout, binary()} - # Standalone text block otherwise matching :stdout - | {:text, binary()} + # Text with terminal style and ANSI support + | {:terminal_text, text :: String.t(), info :: map()} # Plain text content - | {:plain_text, binary()} + | {:plain_text, text :: String.t(), info :: map()} # Markdown content - | {:markdown, binary()} + | {:markdown, text :: String.t(), info :: map()} # A raw image in the given format | {:image, content :: binary(), mime_type :: binary()} # JavaScript powered output diff --git a/lib/livebook/runtime/evaluator/formatter.ex b/lib/livebook/runtime/evaluator/formatter.ex index 251b79952..8e8bad00a 100644 --- a/lib/livebook/runtime/evaluator/formatter.ex +++ b/lib/livebook/runtime/evaluator/formatter.ex @@ -76,7 +76,7 @@ defmodule Livebook.Runtime.Evaluator.Formatter do defp to_inspect_output(value, opts \\ []) do try do inspected = inspect(value, inspect_opts(opts)) - {:text, inspected} + {:terminal_text, inspected, %{chunk: false}} catch kind, error -> formatted = format_error(kind, error, __STACKTRACE__) @@ -172,6 +172,6 @@ defmodule Livebook.Runtime.Evaluator.Formatter do defp erlang_to_output(value) do text = :io_lib.format("~p", [value]) |> IO.iodata_to_binary() - {:text, text} + {:terminal_text, text, %{chunk: false}} end end diff --git a/lib/livebook/runtime/evaluator/io_proxy.ex b/lib/livebook/runtime/evaluator/io_proxy.ex index bde2f6288..37febdd34 100644 --- a/lib/livebook/runtime/evaluator/io_proxy.ex +++ b/lib/livebook/runtime/evaluator/io_proxy.ex @@ -438,7 +438,10 @@ defmodule Livebook.Runtime.Evaluator.IOProxy do string = state.buffer |> Enum.reverse() |> Enum.join() if state.send_to != nil and string != "" do - send(state.send_to, {:runtime_evaluation_output, state.ref, {:stdout, string}}) + send( + state.send_to, + {:runtime_evaluation_output, state.ref, {:terminal_text, string, %{chunk: true}}} + ) end %{state | buffer: []} diff --git a/lib/livebook_web/live/output.ex b/lib/livebook_web/live/output.ex index 315932c35..3163feec4 100644 --- a/lib/livebook_web/live/output.ex +++ b/lib/livebook_web/live/output.ex @@ -37,40 +37,26 @@ defmodule LivebookWeb.Output do """ end - defp border?({:stdout, _text}), do: true - defp border?({:text, _text}), do: true + defp border?({:terminal_text, _text, _info}), do: true + defp border?({:plain_text, _text, _info}), do: true defp border?({:error, _message, {:interrupt, _, _}}), do: false defp border?({:error, _message, _type}), do: true defp border?({:grid, _, info}), do: Map.get(info, :boxed, false) defp border?(_output), do: false - defp render_output({:stdout, text}, %{id: id}) do + defp render_output({:terminal_text, text, _info}, %{id: id}) do text = if(text == :__pruned__, do: nil, else: text) - live_component(Output.StdoutComponent, id: id, text: text) + live_component(Output.TerminalTextComponent, id: id, text: text) end - defp render_output({:text, text}, %{id: id}) do - assigns = %{id: id, text: text} - - ~H""" - - """ + defp render_output({:plain_text, text, _info}, %{id: id}) do + text = if(text == :__pruned__, do: nil, else: text) + live_component(Output.PlainTextComponent, id: id, text: text) end - defp render_output({:plain_text, text}, %{id: id}) do - assigns = %{id: id, text: text} - - ~H""" -
<%= @text %>
- """ - end - - defp render_output({:markdown, markdown}, %{id: id, session_id: session_id}) do - live_component(Output.MarkdownComponent, - id: id, - session_id: session_id, - content: markdown - ) + defp render_output({:markdown, text, _info}, %{id: id, session_id: session_id}) do + text = if(text == :__pruned__, do: nil, else: text) + live_component(Output.MarkdownComponent, id: id, session_id: session_id, text: text) end defp render_output({:image, content, mime_type}, %{id: id}) do diff --git a/lib/livebook_web/live/output/markdown_component.ex b/lib/livebook_web/live/output/markdown_component.ex index a464bcf55..84b7ce68e 100644 --- a/lib/livebook_web/live/output/markdown_component.ex +++ b/lib/livebook_web/live/output/markdown_component.ex @@ -3,30 +3,41 @@ defmodule LivebookWeb.Output.MarkdownComponent do @impl true def mount(socket) do - {:ok, assign(socket, allowed_uri_schemes: Livebook.Config.allowed_uri_schemes())} + {:ok, assign(socket, allowed_uri_schemes: Livebook.Config.allowed_uri_schemes(), chunks: 0), + temporary_assigns: [text: nil]} end @impl true def update(assigns, socket) do + {text, assigns} = Map.pop(assigns, :text) socket = assign(socket, assigns) - {:ok, - push_event(socket, "markdown_renderer:#{socket.assigns.id}:content", %{ - content: socket.assigns.content - })} + if text do + {:ok, socket |> assign(text: text) |> update(:chunks, &(&1 + 1))} + else + {:ok, socket} + end end @impl true def render(assigns) do ~H"""
+ +
+
""" end diff --git a/lib/livebook_web/live/output/plain_text_component.ex b/lib/livebook_web/live/output/plain_text_component.ex new file mode 100644 index 000000000..820b2f3bc --- /dev/null +++ b/lib/livebook_web/live/output/plain_text_component.ex @@ -0,0 +1,33 @@ +defmodule LivebookWeb.Output.PlainTextComponent do + use LivebookWeb, :live_component + + @impl true + def mount(socket) do + {:ok, assign(socket, chunks: 0), temporary_assigns: [text: nil]} + end + + @impl true + def update(assigns, socket) do + {text, assigns} = Map.pop(assigns, :text) + socket = assign(socket, assigns) + + if text do + {:ok, socket |> assign(text: text) |> update(:chunks, &(&1 + 1))} + else + {:ok, socket} + end + end + + @impl true + def render(assigns) do + ~H""" +
<%= + @text %>
+ """ + end +end diff --git a/lib/livebook_web/live/output/stdout_component.ex b/lib/livebook_web/live/output/terminal_text_component.ex similarity index 93% rename from lib/livebook_web/live/output/stdout_component.ex rename to lib/livebook_web/live/output/terminal_text_component.ex index 4774b8428..0df544212 100644 --- a/lib/livebook_web/live/output/stdout_component.ex +++ b/lib/livebook_web/live/output/terminal_text_component.ex @@ -1,4 +1,4 @@ -defmodule LivebookWeb.Output.StdoutComponent do +defmodule LivebookWeb.Output.TerminalTextComponent do use LivebookWeb, :live_component @impl true @@ -15,7 +15,7 @@ defmodule LivebookWeb.Output.StdoutComponent do if text do text = (socket.assigns.last_line || "") <> text - text = Livebook.Notebook.normalize_stdout(text) + text = Livebook.Notebook.normalize_terminal_text(text) last_line = case Livebook.Utils.split_at_last_occurrence(text, "\n") do @@ -49,7 +49,7 @@ defmodule LivebookWeb.Output.StdoutComponent do phx-hook="VirtualizedLines" data-max-height="300" data-follow="true" - data-max-lines={Livebook.Notebook.max_stdout_lines()} + data-max-lines={Livebook.Notebook.max_terminal_lines()} data-ignore-trailing-empty-line="true" > <% # Note 1: We add a newline to each element, so that multiple lines can be copied properly as element.textContent %> diff --git a/lib/livebook_web/live/output/text_component.ex b/lib/livebook_web/live/output/text_component.ex deleted file mode 100644 index 012775f08..000000000 --- a/lib/livebook_web/live/output/text_component.ex +++ /dev/null @@ -1,38 +0,0 @@ -defmodule LivebookWeb.Output.TextComponent do - use LivebookWeb, :live_component - - @impl true - def render(assigns) do - ~H""" -
- <% # Add a newline to each element, so that multiple lines can be copied properly %> - -
-
-
- -
-
- """ - end -end diff --git a/lib/livebook_web/live/session_live.ex b/lib/livebook_web/live/session_live.ex index f673ef55e..12032ec48 100644 --- a/lib/livebook_web/live/session_live.ex +++ b/lib/livebook_web/live/session_live.ex @@ -2783,11 +2783,19 @@ defmodule LivebookWeb.SessionLive do data_view - {:add_cell_evaluation_output, _client_id, cell_id, {:stdout, text}} -> + {:add_cell_evaluation_output, _client_id, cell_id, {type, text, %{chunk: true}}} + when type in [:terminal_text, :plain_text, :markdown] -> # Lookup in previous data to see if the output is already there case Notebook.fetch_cell_and_section(prev_data.notebook, cell_id) do - {:ok, %{outputs: [{idx, {:stdout, _}} | _]}, _section} -> - send_update(LivebookWeb.Output.StdoutComponent, id: "output-#{idx}", text: text) + {:ok, %{outputs: [{idx, {^type, _, %{chunk: true}}} | _]}, _section} -> + module = + case type do + :terminal_text -> LivebookWeb.Output.TerminalTextComponent + :plain_text -> LivebookWeb.Output.PlainTextComponent + :markdown -> LivebookWeb.Output.MarkdownComponent + end + + send_update(module, id: "output-#{idx}", text: text) data_view _ -> diff --git a/test/livebook/live_markdown/export_test.exs b/test/livebook/live_markdown/export_test.exs index 1cfcb7cf0..869025594 100644 --- a/test/livebook/live_markdown/export_test.exs +++ b/test/livebook/live_markdown/export_test.exs @@ -577,7 +577,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do IO.puts("hey")\ """, outputs: [ - {0, {:stdout, "hey"}} + {0, {:terminal_text, "hey", %{chunk: true}}} ] } ] @@ -614,7 +614,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do | source: """ IO.puts("hey")\ """, - outputs: [{0, {:stdout, "hey"}}] + outputs: [{0, {:terminal_text, "hey", %{chunk: true}}}] } ] } @@ -656,7 +656,10 @@ defmodule Livebook.LiveMarkdown.ExportTest do | source: """ IO.puts("hey")\ """, - outputs: [{0, {:text, "\e[34m:ok\e[0m"}}, {1, {:stdout, "hey"}}] + outputs: [ + {0, {:terminal_text, "\e[34m:ok\e[0m", %{chunk: false}}}, + {1, {:terminal_text, "hey", %{chunk: true}}} + ] } ] } @@ -947,8 +950,8 @@ defmodule Livebook.LiveMarkdown.ExportTest do {:tabs, [ {1, {:markdown, "a"}}, - {2, {:text, "b"}}, - {3, {:text, "c"}} + {2, {:terminal_text, "b", %{chunk: false}}}, + {3, {:terminal_text, "c", %{chunk: false}}} ], %{labels: ["A", "B", "C"]}}} ] } @@ -994,9 +997,9 @@ defmodule Livebook.LiveMarkdown.ExportTest do {0, {:grid, [ - {1, {:text, "a"}}, + {1, {:terminal_text, "a", %{chunk: false}}}, {2, {:markdown, "b"}}, - {3, {:text, "c"}} + {3, {:terminal_text, "c", %{chunk: false}}} ], %{columns: 2}}} ] } @@ -1047,7 +1050,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do | source: """ IO.puts("hey")\ """, - outputs: [{0, {:stdout, "hey"}}] + outputs: [{0, {:terminal_text, "hey", %{chunk: true}}}] } ] } @@ -1092,7 +1095,7 @@ defmodule Livebook.LiveMarkdown.ExportTest do | source: """ IO.puts("hey")\ """, - outputs: [{0, {:stdout, "hey"}}] + outputs: [{0, {:terminal_text, "hey", %{chunk: true}}}] } ] } diff --git a/test/livebook/live_markdown/import_test.exs b/test/livebook/live_markdown/import_test.exs index 451264bd5..34e80f484 100644 --- a/test/livebook/live_markdown/import_test.exs +++ b/test/livebook/live_markdown/import_test.exs @@ -642,7 +642,10 @@ defmodule Livebook.LiveMarkdown.ImportTest do source: """ IO.puts("hey")\ """, - outputs: [{0, {:text, ":ok"}}, {1, {:text, "hey"}}] + outputs: [ + {0, {:terminal_text, ":ok", %{chunk: false}}}, + {1, {:terminal_text, "hey", %{chunk: false}}} + ] } ] } @@ -882,7 +885,10 @@ defmodule Livebook.LiveMarkdown.ImportTest do source: """ IO.puts("hey")\ """, - outputs: [{0, {:text, ":ok"}}, {1, {:text, "hey"}}] + outputs: [ + {0, {:terminal_text, ":ok", %{chunk: false}}}, + {1, {:terminal_text, "hey", %{chunk: false}}} + ] } ] } diff --git a/test/livebook/notebook_test.exs b/test/livebook/notebook_test.exs index 06b6c48f5..9d64e59ed 100644 --- a/test/livebook/notebook_test.exs +++ b/test/livebook/notebook_test.exs @@ -282,7 +282,7 @@ defmodule Livebook.NotebookTest do end describe "add_cell_output/3" do - test "merges consecutive stdout results" do + test "merges consecutive text outputs when both are chunks" do notebook = %{ Notebook.new() | sections: [ @@ -290,7 +290,11 @@ defmodule Livebook.NotebookTest do Section.new() | id: "s1", cells: [ - %{Cell.new(:code) | id: "c1", outputs: [{0, {:stdout, "Hola"}}]} + %{ + Cell.new(:code) + | id: "c1", + outputs: [{0, {:terminal_text, "Hola", %{chunk: true}}}] + } ] } ], @@ -300,13 +304,58 @@ defmodule Livebook.NotebookTest do assert %{ sections: [ %{ - cells: [%{outputs: [{0, {:stdout, "Hola amigo!"}}]}] + cells: [%{outputs: [{0, {:terminal_text, "Hola amigo!", %{chunk: true}}}]}] } ] - } = Notebook.add_cell_output(notebook, "c1", {:stdout, " amigo!"}) + } = + Notebook.add_cell_output( + notebook, + "c1", + {:terminal_text, " amigo!", %{chunk: true}} + ) end - test "normalizes individual stdout results to respect CR" do + test "adds separate text outputs when they are not chunks" do + notebook = %{ + Notebook.new() + | sections: [ + %{ + Section.new() + | id: "s1", + cells: [ + %{ + Cell.new(:code) + | id: "c1", + outputs: [{0, {:terminal_text, "Hola", %{chunk: false}}}] + } + ] + } + ], + output_counter: 1 + } + + assert %{ + sections: [ + %{ + cells: [ + %{ + outputs: [ + {1, {:terminal_text, " amigo!", %{chunk: true}}}, + {0, {:terminal_text, "Hola", %{chunk: false}}} + ] + } + ] + } + ] + } = + Notebook.add_cell_output( + notebook, + "c1", + {:terminal_text, " amigo!", %{chunk: true}} + ) + end + + test "normalizes individual terminal text outputs to respect CR" do notebook = %{ Notebook.new() | sections: [ @@ -324,13 +373,18 @@ defmodule Livebook.NotebookTest do assert %{ sections: [ %{ - cells: [%{outputs: [{0, {:stdout, "Hey"}}]}] + cells: [%{outputs: [{0, {:terminal_text, "Hey", %{chunk: false}}}]}] } ] - } = Notebook.add_cell_output(notebook, "c1", {:stdout, "Hola\rHey"}) + } = + Notebook.add_cell_output( + notebook, + "c1", + {:terminal_text, "Hola\rHey", %{chunk: false}} + ) end - test "normalizes consecutive stdout results to respect CR" do + test "normalizes consecutive terminal text outputs to respect CR" do notebook = %{ Notebook.new() | sections: [ @@ -338,7 +392,11 @@ defmodule Livebook.NotebookTest do Section.new() | id: "s1", cells: [ - %{Cell.new(:code) | id: "c1", outputs: [{0, {:stdout, "Hola"}}]} + %{ + Cell.new(:code) + | id: "c1", + outputs: [{0, {:terminal_text, "Hola", %{chunk: true}}}] + } ] } ], @@ -348,10 +406,15 @@ defmodule Livebook.NotebookTest do assert %{ sections: [ %{ - cells: [%{outputs: [{0, {:stdout, "amigo!\r"}}]}] + cells: [%{outputs: [{0, {:terminal_text, "amigo!\r", %{chunk: true}}}]}] } ] - } = Notebook.add_cell_output(notebook, "c1", {:stdout, "\ramigo!\r"}) + } = + Notebook.add_cell_output( + notebook, + "c1", + {:terminal_text, "\ramigo!\r", %{chunk: true}} + ) end test "updates existing frames on frame update output" do @@ -384,12 +447,16 @@ defmodule Livebook.NotebookTest do cells: [ %{ outputs: [ - {0, {:frame, [{2, {:text, "hola"}}], %{ref: "1", type: :default}}} + {0, + {:frame, [{2, {:terminal_text, "hola", %{chunk: false}}}], + %{ref: "1", type: :default}}} ] }, %{ outputs: [ - {1, {:frame, [{3, {:text, "hola"}}], %{ref: "1", type: :default}}} + {1, + {:frame, [{3, {:terminal_text, "hola", %{chunk: false}}}], + %{ref: "1", type: :default}}} ] } ] @@ -399,7 +466,47 @@ defmodule Livebook.NotebookTest do Notebook.add_cell_output( notebook, "c2", - {:frame, [{:text, "hola"}], %{ref: "1", type: :replace}} + {:frame, [{:terminal_text, "hola", %{chunk: false}}], + %{ref: "1", type: :replace}} + ) + end + + test "merges chunked text outputs in a new frame" do + notebook = %{ + Notebook.new() + | sections: [ + %{ + Section.new() + | id: "s1", + cells: [%{Cell.new(:code) | id: "c1", outputs: []}] + } + ], + output_counter: 0 + } + + assert %{ + sections: [ + %{ + cells: [ + %{ + outputs: [ + {2, + {:frame, [{1, {:terminal_text, "hola amigo!", %{chunk: true}}}], + %{ref: "1", type: :default}}} + ] + } + ] + } + ] + } = + Notebook.add_cell_output( + notebook, + "c1", + {:frame, + [ + {:terminal_text, " amigo!", %{chunk: true}}, + {:terminal_text, "hola", %{chunk: true}} + ], %{ref: "1", type: :default}} ) end @@ -424,6 +531,36 @@ defmodule Livebook.NotebookTest do ] } = Notebook.add_cell_output(notebook, "c1", :ignored) end + + test "supports legacy text outputs" do + notebook = %{ + Notebook.new() + | sections: [ + %{ + Section.new() + | id: "s1", + cells: [%{Cell.new(:code) | id: "c1", outputs: []}] + } + ], + output_counter: 0 + } + + assert %{ + sections: [ + %{ + cells: [%{outputs: [{0, {:terminal_text, "Hola", %{chunk: false}}}]}] + } + ] + } = Notebook.add_cell_output(notebook, "c1", {:text, "Hola"}) + + assert %{ + sections: [ + %{ + cells: [%{outputs: [{0, {:markdown, "Hola", %{chunk: false}}}]}] + } + ] + } = Notebook.add_cell_output(notebook, "c1", {:markdown, "Hola"}) + end end describe "find_frame_outputs/2" do diff --git a/test/livebook/runtime/erl_dist/runtime_server_test.exs b/test/livebook/runtime/erl_dist/runtime_server_test.exs index 919f8581d..362d23705 100644 --- a/test/livebook/runtime/erl_dist/runtime_server_test.exs +++ b/test/livebook/runtime/erl_dist/runtime_server_test.exs @@ -63,7 +63,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do [] ) - assert_receive {:runtime_evaluation_output, :e1, {:stdout, output}} + assert_receive {:runtime_evaluation_output, :e1, {:terminal_text, output, %{chunk: true}}} assert output =~ "error to stdout\n" end @@ -77,7 +77,9 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do RuntimeServer.evaluate_code(pid, :elixir, code, {:c1, :e1}, []) - assert_receive {:runtime_evaluation_output, :e1, {:stdout, log_message}} + assert_receive {:runtime_evaluation_output, :e1, + {:terminal_text, log_message, %{chunk: true}}} + assert log_message =~ "[error] hey" end @@ -87,7 +89,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do RuntimeServer.evaluate_code(pid, :elixir, "x", {:c2, :e2}, [{:c1, :e1}]) - assert_receive {:runtime_evaluation_response, :e2, {:text, "\e[34m1\e[0m"}, + assert_receive {:runtime_evaluation_response, :e2, {:terminal_text, "\e[34m1\e[0m", %{}}, %{evaluation_time_ms: _time_ms}} end diff --git a/test/livebook/runtime/evaluator/io_proxy_test.exs b/test/livebook/runtime/evaluator/io_proxy_test.exs index 996655491..a9593e446 100644 --- a/test/livebook/runtime/evaluator/io_proxy_test.exs +++ b/test/livebook/runtime/evaluator/io_proxy_test.exs @@ -18,17 +18,17 @@ defmodule Livebook.Runtime.Evaluator.IOProxyTest do describe ":stdio interoperability" do test "IO.puts", %{io: io} do IO.puts(io, "hey") - assert_receive {:runtime_evaluation_output, :ref, {:stdout, "hey\n"}} + assert_receive {:runtime_evaluation_output, :ref, {:terminal_text, "hey\n", %{chunk: true}}} end test "IO.write", %{io: io} do IO.write(io, "hey") - assert_receive {:runtime_evaluation_output, :ref, {:stdout, "hey"}} + assert_receive {:runtime_evaluation_output, :ref, {:terminal_text, "hey", %{chunk: true}}} end test "IO.inspect", %{io: io} do IO.inspect(io, %{}, []) - assert_receive {:runtime_evaluation_output, :ref, {:stdout, "%{}\n"}} + assert_receive {:runtime_evaluation_output, :ref, {:terminal_text, "%{}\n", %{chunk: true}}} end test "IO.read", %{io: io} do @@ -83,31 +83,37 @@ defmodule Livebook.Runtime.Evaluator.IOProxyTest do test "buffers rapid output", %{io: io} do IO.puts(io, "hey") IO.puts(io, "hey") - assert_receive {:runtime_evaluation_output, :ref, {:stdout, "hey\nhey\n"}} + + assert_receive {:runtime_evaluation_output, :ref, + {:terminal_text, "hey\nhey\n", %{chunk: true}}} end test "respects CR as line cleaner", %{io: io} do IO.write(io, "hey") IO.write(io, "\roverride\r") - assert_receive {:runtime_evaluation_output, :ref, {:stdout, "\roverride\r"}} + + assert_receive {:runtime_evaluation_output, :ref, + {:terminal_text, "\roverride\r", %{chunk: true}}} end test "after_evaluation/1 synchronously sends buffer contents", %{io: io} do IO.puts(io, "hey") IOProxy.after_evaluation(io) - assert_received {:runtime_evaluation_output, :ref, {:stdout, "hey\n"}} + assert_received {:runtime_evaluation_output, :ref, {:terminal_text, "hey\n", %{chunk: true}}} end test "supports direct livebook output forwarding", %{io: io} do - livebook_put_output(io, {:text, "[1, 2, 3]"}) + livebook_put_output(io, {:terminal_text, "[1, 2, 3]", %{chunk: false}}) - assert_received {:runtime_evaluation_output, :ref, {:text, "[1, 2, 3]"}} + assert_received {:runtime_evaluation_output, :ref, + {:terminal_text, "[1, 2, 3]", %{chunk: false}}} end test "supports direct livebook output forwarding for a specific client", %{io: io} do - livebook_put_output_to(io, "client1", {:text, "[1, 2, 3]"}) + livebook_put_output_to(io, "client1", {:terminal_text, "[1, 2, 3]", %{chunk: false}}) - assert_received {:runtime_evaluation_output_to, "client1", :ref, {:text, "[1, 2, 3]"}} + assert_received {:runtime_evaluation_output_to, "client1", :ref, + {:terminal_text, "[1, 2, 3]", %{chunk: false}}} end describe "token requests" do diff --git a/test/livebook/runtime/evaluator_test.exs b/test/livebook/runtime/evaluator_test.exs index 3d0ac4ec0..af0719a25 100644 --- a/test/livebook/runtime/evaluator_test.exs +++ b/test/livebook/runtime/evaluator_test.exs @@ -49,8 +49,8 @@ defmodule Livebook.Runtime.EvaluatorTest do Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, []) - assert_receive {:runtime_evaluation_response, :code_1, {:text, ansi_number(3)}, - metadata() = metadata} + assert_receive {:runtime_evaluation_response, :code_1, + {:terminal_text, ansi_number(3), %{}}, metadata() = metadata} assert metadata.evaluation_time_ms >= 0 @@ -76,13 +76,15 @@ defmodule Livebook.Runtime.EvaluatorTest do Evaluator.evaluate_code(evaluator, :elixir, "x", :code_2, [:code_1]) - assert_receive {:runtime_evaluation_response, :code_2, {:text, ansi_number(1)}, metadata()} + assert_receive {:runtime_evaluation_response, :code_2, + {:terminal_text, ansi_number(1), %{}}, metadata()} end test "given invalid parent ref uses the default context", %{evaluator: evaluator} do Evaluator.evaluate_code(evaluator, :elixir, "1", :code_1, [:code_nonexistent]) - assert_receive {:runtime_evaluation_response, :code_1, {:text, ansi_number(1)}, metadata()} + assert_receive {:runtime_evaluation_response, :code_1, + {:terminal_text, ansi_number(1), %{}}, metadata()} end test "given parent refs sees previous process dictionary", %{evaluator: evaluator} do @@ -92,10 +94,14 @@ defmodule Livebook.Runtime.EvaluatorTest do assert_receive {:runtime_evaluation_response, :code_2, _, metadata()} Evaluator.evaluate_code(evaluator, :elixir, "Process.get(:x)", :code_3, [:code_1]) - assert_receive {:runtime_evaluation_response, :code_3, {:text, ansi_number(1)}, metadata()} + + assert_receive {:runtime_evaluation_response, :code_3, + {:terminal_text, ansi_number(1), %{}}, metadata()} Evaluator.evaluate_code(evaluator, :elixir, "Process.get(:x)", :code_3, [:code_2]) - assert_receive {:runtime_evaluation_response, :code_3, {:text, ansi_number(2)}, metadata()} + + assert_receive {:runtime_evaluation_response, :code_3, + {:terminal_text, ansi_number(2), %{}}, metadata()} end test "keeps :rand state intact in process dictionary", %{evaluator: evaluator} do @@ -103,10 +109,14 @@ defmodule Livebook.Runtime.EvaluatorTest do assert_receive {:runtime_evaluation_response, :code_1, _, metadata()} Evaluator.evaluate_code(evaluator, :elixir, ":rand.uniform()", :code_2, []) - assert_receive {:runtime_evaluation_response, :code_2, {:text, result1}, metadata()} + + assert_receive {:runtime_evaluation_response, :code_2, {:terminal_text, result1, %{}}, + metadata()} Evaluator.evaluate_code(evaluator, :elixir, ":rand.uniform()", :code_2, []) - assert_receive {:runtime_evaluation_response, :code_2, {:text, result2}, metadata()} + + assert_receive {:runtime_evaluation_response, :code_2, {:terminal_text, result2, %{}}, + metadata()} assert result1 != result2 @@ -114,16 +124,21 @@ defmodule Livebook.Runtime.EvaluatorTest do assert_receive {:runtime_evaluation_response, :code_1, _, metadata()} Evaluator.evaluate_code(evaluator, :elixir, ":rand.uniform()", :code_2, []) - assert_receive {:runtime_evaluation_response, :code_2, {:text, ^result1}, metadata()} + + assert_receive {:runtime_evaluation_response, :code_2, {:terminal_text, ^result1, %{}}, + metadata()} Evaluator.evaluate_code(evaluator, :elixir, ":rand.uniform()", :code_2, []) - assert_receive {:runtime_evaluation_response, :code_2, {:text, ^result2}, metadata()} + + assert_receive {:runtime_evaluation_response, :code_2, {:terminal_text, ^result2, %{}}, + metadata()} end test "captures standard output and sends it to the caller", %{evaluator: evaluator} do Evaluator.evaluate_code(evaluator, :elixir, ~s{IO.puts("hey")}, :code_1, []) - assert_receive {:runtime_evaluation_output, :code_1, {:stdout, "hey\n"}} + assert_receive {:runtime_evaluation_output, :code_1, + {:terminal_text, "hey\n", %{chunk: true}}} end test "using livebook input sends input request to the caller", %{evaluator: evaluator} do @@ -141,7 +156,8 @@ defmodule Livebook.Runtime.EvaluatorTest do assert_receive {:runtime_evaluation_input_request, :code_1, reply_to, "input1"} send(reply_to, {:runtime_evaluation_input_reply, {:ok, 10}}) - assert_receive {:runtime_evaluation_response, :code_1, {:text, ansi_number(10)}, metadata()} + assert_receive {:runtime_evaluation_response, :code_1, + {:terminal_text, ansi_number(10), %{}}, metadata()} end test "returns error along with its kind and stacktrace", %{evaluator: evaluator} do @@ -307,14 +323,18 @@ defmodule Livebook.Runtime.EvaluatorTest do """ Evaluator.evaluate_code(evaluator, :elixir, code1, :code_1, []) - assert_receive {:runtime_evaluation_response, :code_1, {:text, ansi_number(2)}, metadata()} + + assert_receive {:runtime_evaluation_response, :code_1, + {:terminal_text, ansi_number(2), %{}}, metadata()} Evaluator.evaluate_code(evaluator, :elixir, code2, :code_2, [:code_1]) assert_receive {:runtime_evaluation_response, :code_2, {:error, _, _}, metadata()} Evaluator.evaluate_code(evaluator, :elixir, code3, :code_3, [:code_2, :code_1]) - assert_receive {:runtime_evaluation_response, :code_3, {:text, ansi_number(4)}, metadata()} + + assert_receive {:runtime_evaluation_response, :code_3, + {:terminal_text, ansi_number(4), %{}}, metadata()} end test "given file option sets it in evaluation environment", %{evaluator: evaluator} do @@ -325,8 +345,8 @@ defmodule Livebook.Runtime.EvaluatorTest do opts = [file: "/path/dir/file"] Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [], opts) - assert_receive {:runtime_evaluation_response, :code_1, {:text, ansi_string("/path/dir")}, - metadata()} + assert_receive {:runtime_evaluation_response, :code_1, + {:terminal_text, ansi_string("/path/dir"), %{}}, metadata()} end test "kills widgets that that no evaluation points to", %{evaluator: evaluator} do @@ -336,8 +356,8 @@ defmodule Livebook.Runtime.EvaluatorTest do Evaluator.evaluate_code(evaluator, :elixir, spawn_widget_code(), :code_1, []) - assert_receive {:runtime_evaluation_response, :code_1, {:text, widget_pid1_string}, - metadata()} + assert_receive {:runtime_evaluation_response, :code_1, + {:terminal_text, widget_pid1_string, %{}}, metadata()} widget_pid1 = IEx.Helpers.pid(widget_pid1_string) @@ -345,8 +365,8 @@ defmodule Livebook.Runtime.EvaluatorTest do Evaluator.evaluate_code(evaluator, :elixir, spawn_widget_code(), :code_1, []) - assert_receive {:runtime_evaluation_response, :code_1, {:text, widget_pid2_string}, - metadata()} + assert_receive {:runtime_evaluation_response, :code_1, + {:terminal_text, widget_pid2_string, %{}}, metadata()} widget_pid2 = IEx.Helpers.pid(widget_pid2_string) @@ -367,8 +387,8 @@ defmodule Livebook.Runtime.EvaluatorTest do [] ) - assert_receive {:runtime_evaluation_response, :code_1, {:text, widget_pid1_string}, - metadata()} + assert_receive {:runtime_evaluation_response, :code_1, + {:terminal_text, widget_pid1_string, %{}}, metadata()} widget_pid1 = IEx.Helpers.pid(widget_pid1_string) @@ -384,11 +404,11 @@ defmodule Livebook.Runtime.EvaluatorTest do """ Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, []) - assert_receive {:runtime_evaluation_response, :code_1, {:text, _}, metadata()} + assert_receive {:runtime_evaluation_response, :code_1, {:terminal_text, _, %{}}, metadata()} # Redefining in the same evaluation works Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, []) - assert_receive {:runtime_evaluation_response, :code_1, {:text, _}, metadata()} + assert_receive {:runtime_evaluation_response, :code_1, {:terminal_text, _, %{}}, metadata()} Evaluator.evaluate_code(evaluator, :elixir, code, :code_2, [], file: "file.ex") @@ -418,7 +438,7 @@ defmodule Livebook.Runtime.EvaluatorTest do """ Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, []) - assert_receive {:runtime_evaluation_response, :code_1, {:text, _}, metadata()} + assert_receive {:runtime_evaluation_response, :code_1, {:terminal_text, _, %{}}, metadata()} assert File.exists?(Path.join(ebin_path, "Elixir.Livebook.Runtime.EvaluatorTest.Disk.beam")) @@ -722,7 +742,7 @@ defmodule Livebook.Runtime.EvaluatorTest do ref = eval_idx parent_refs = Enum.to_list((eval_idx - 1)..0//-1) Evaluator.evaluate_code(evaluator, :elixir, code, ref, parent_refs) - assert_receive {:runtime_evaluation_response, ^ref, {:text, _}, metadata} + assert_receive {:runtime_evaluation_response, ^ref, {:terminal_text, _, %{}}, metadata} %{used: metadata.identifiers_used, defined: metadata.identifiers_defined} end @@ -1138,8 +1158,8 @@ defmodule Livebook.Runtime.EvaluatorTest do test "kills widgets that no evaluation points to", %{evaluator: evaluator} do Evaluator.evaluate_code(evaluator, :elixir, spawn_widget_code(), :code_1, []) - assert_receive {:runtime_evaluation_response, :code_1, {:text, widget_pid1_string}, - metadata()} + assert_receive {:runtime_evaluation_response, :code_1, + {:terminal_text, widget_pid1_string, %{}}, metadata()} widget_pid1 = IEx.Helpers.pid(widget_pid1_string) @@ -1157,13 +1177,13 @@ defmodule Livebook.Runtime.EvaluatorTest do """ Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, []) - assert_receive {:runtime_evaluation_response, :code_1, {:text, _}, metadata()} + assert_receive {:runtime_evaluation_response, :code_1, {:terminal_text, _, %{}}, metadata()} Evaluator.forget_evaluation(evaluator, :code_1) # Define the module in a different evaluation Evaluator.evaluate_code(evaluator, :elixir, code, :code_2, []) - assert_receive {:runtime_evaluation_response, :code_2, {:text, _}, metadata()} + assert_receive {:runtime_evaluation_response, :code_2, {:terminal_text, _, %{}}, metadata()} end end @@ -1185,7 +1205,9 @@ defmodule Livebook.Runtime.EvaluatorTest do Evaluator.initialize_from(evaluator, parent_evaluator, [:code_1]) Evaluator.evaluate_code(evaluator, :elixir, "x", :code_2, []) - assert_receive {:runtime_evaluation_response, :code_2, {:text, ansi_number(1)}, metadata()} + + assert_receive {:runtime_evaluation_response, :code_2, + {:terminal_text, ansi_number(1), %{}}, metadata()} end end @@ -1224,7 +1246,8 @@ defmodule Livebook.Runtime.EvaluatorTest do [] ) - assert_receive {:runtime_evaluation_response, :code_1, {:text, "6"}, metadata()} + assert_receive {:runtime_evaluation_response, :code_1, {:terminal_text, "6", %{}}, + metadata()} end test "mixed erlang/elixir bindings", %{evaluator: evaluator} do @@ -1242,7 +1265,8 @@ defmodule Livebook.Runtime.EvaluatorTest do code = ~S"#{x=>1}." Evaluator.evaluate_code(evaluator, :erlang, code, :code_1, [], file: "file.ex") - assert_receive {:runtime_evaluation_response, :code_1, {:text, ~S"#{x => 1}"}, metadata()} + assert_receive {:runtime_evaluation_response, :code_1, {:terminal_text, ~S"#{x => 1}", %{}}, + metadata()} end test "does not return error marker on empty source", %{evaluator: evaluator} do diff --git a/test/livebook/session/data_test.exs b/test/livebook/session/data_test.exs index a68c12337..db0ce215a 100644 --- a/test/livebook/session/data_test.exs +++ b/test/livebook/session/data_test.exs @@ -9,6 +9,7 @@ defmodule Livebook.Session.DataTest do @eval_resp {:ok, [1, 2, 3]} @smart_cell_definitions [%{kind: "text", name: "Text", requirement_presets: []}] + @stdout {:terminal_text, "Hello!", %{chunk: true}} @cid "__anonymous__" defp eval_meta(opts \\ []) do @@ -78,13 +79,13 @@ defmodule Livebook.Session.DataTest do Notebook.Section.new() | id: "s1", cells: [ - %{Notebook.Cell.new(:code) | id: "c1", outputs: [{0, {:stdout, "Hello!"}}]} + %{Notebook.Cell.new(:code) | id: "c1", outputs: [{0, @stdout}]} ] } ] } - assert %{notebook: %{sections: [%{cells: [%{outputs: [{0, {:stdout, "Hello!"}}]}]}]}} = + assert %{notebook: %{sections: [%{cells: [%{outputs: [{0, @stdout}]}]}]}} = Data.new(notebook: notebook) assert %{notebook: %{sections: [%{cells: [%{outputs: []}]}]}} = @@ -1819,14 +1820,14 @@ defmodule Livebook.Session.DataTest do {:queue_cells_evaluation, @cid, ["c1"]} ]) - operation = {:add_cell_evaluation_output, @cid, "c1", {:stdout, "Hello!"}} + operation = {:add_cell_evaluation_output, @cid, "c1", @stdout} assert {:ok, %{ notebook: %{ sections: [ %{ - cells: [%{outputs: [{1, {:stdout, "Hello!"}}]}] + cells: [%{outputs: [{1, @stdout}]}] } ] } @@ -1842,14 +1843,14 @@ defmodule Livebook.Session.DataTest do evaluate_cells_operations(["setup", "c1"]) ]) - operation = {:add_cell_evaluation_output, @cid, "c1", {:stdout, "Hello!"}} + operation = {:add_cell_evaluation_output, @cid, "c1", @stdout} assert {:ok, %{ notebook: %{ sections: [ %{ - cells: [%{outputs: [{2, {:stdout, "Hello!"}}, _result]}] + cells: [%{outputs: [{2, @stdout}, _result]}] } ] } @@ -1868,7 +1869,7 @@ defmodule Livebook.Session.DataTest do {:notebook_saved, @cid, []} ]) - operation = {:add_cell_evaluation_output, @cid, "c1", {:stdout, "Hello!"}} + operation = {:add_cell_evaluation_output, @cid, "c1", @stdout} assert {:ok, %{dirty: true}, []} = Data.apply_operation(data, operation) end diff --git a/test/livebook/session_test.exs b/test/livebook/session_test.exs index 770e90a59..d5a209439 100644 --- a/test/livebook/session_test.exs +++ b/test/livebook/session_test.exs @@ -187,7 +187,7 @@ defmodule Livebook.SessionTest do | kind: "text", source: "chunk 1\n\nchunk 2", chunks: [{0, 7}, {9, 7}], - outputs: [{1, {:text, "Hello"}}] + outputs: [{1, {:terminal_text, "Hello", %{chunk: false}}}] } section = %{Notebook.Section.new() | cells: [smart_cell]} @@ -210,7 +210,7 @@ defmodule Livebook.SessionTest do assert_receive {:operation, {:insert_cell, _client_id, ^section_id, 1, :code, _id, - %{source: "chunk 2", outputs: [{1, {:text, "Hello"}}]}}} + %{source: "chunk 2", outputs: [{1, {:terminal_text, "Hello", %{}}}]}}} end test "doesn't garbage collect input values" do @@ -920,8 +920,8 @@ defmodule Livebook.SessionTest do Session.queue_cell_evaluation(session.pid, cell_id) assert_receive {:operation, - {:add_cell_evaluation_response, _, ^cell_id, {:text, text_output}, - %{evaluation_time_ms: _time_ms}}} + {:add_cell_evaluation_response, _, ^cell_id, + {:terminal_text, text_output, %{}}, %{evaluation_time_ms: _time_ms}}} assert text_output =~ "hey" end @@ -944,8 +944,8 @@ defmodule Livebook.SessionTest do Session.queue_cell_evaluation(session.pid, cell_id) assert_receive {:operation, - {:add_cell_evaluation_response, _, ^cell_id, {:text, text_output}, - %{evaluation_time_ms: _time_ms}}} + {:add_cell_evaluation_response, _, ^cell_id, + {:terminal_text, text_output, %{}}, %{evaluation_time_ms: _time_ms}}} assert text_output =~ ":error" end diff --git a/test/livebook_teams/web/session_live_test.exs b/test/livebook_teams/web/session_live_test.exs index 1f18c445b..eaf9ae443 100644 --- a/test/livebook_teams/web/session_live_test.exs +++ b/test/livebook_teams/web/session_live_test.exs @@ -207,7 +207,8 @@ defmodule LivebookWeb.Integration.SessionLiveTest do Session.queue_cell_evaluation(session.pid, cell_id) assert_receive {:operation, - {:add_cell_evaluation_response, _, ^cell_id, {:text, output}, _}} + {:add_cell_evaluation_response, _, ^cell_id, {:terminal_text, output, %{}}, + _}} assert output == "\e[32m\"#{secret.value}\"\e[0m" end @@ -271,7 +272,8 @@ defmodule LivebookWeb.Integration.SessionLiveTest do Session.queue_cell_evaluation(session.pid, cell_id) assert_receive {:operation, - {:add_cell_evaluation_response, _, ^cell_id, {:text, output}, _}} + {:add_cell_evaluation_response, _, ^cell_id, {:terminal_text, output, %{}}, + _}} assert output == "\e[32m\"#{secret.value}\"\e[0m" end diff --git a/test/livebook_web/controllers/session_controller_test.exs b/test/livebook_web/controllers/session_controller_test.exs index 7b3a9c4cb..05ec8a078 100644 --- a/test/livebook_web/controllers/session_controller_test.exs +++ b/test/livebook_web/controllers/session_controller_test.exs @@ -221,7 +221,7 @@ defmodule LivebookWeb.SessionControllerTest do | source: """ IO.puts("hey")\ """, - outputs: [{0, {:stdout, "hey"}}] + outputs: [{0, {:terminal_text, "hey", %{chunk: true}}}] } ] } diff --git a/test/livebook_web/live/app_session_live_test.exs b/test/livebook_web/live/app_session_live_test.exs index ca7a9694a..521a97fa8 100644 --- a/test/livebook_web/live/app_session_live_test.exs +++ b/test/livebook_web/live/app_session_live_test.exs @@ -81,11 +81,11 @@ defmodule LivebookWeb.AppSessionLiveTest do | cells: [ %{ Livebook.Notebook.Cell.new(:code) - | source: source_for_output({:stdout, "Printed output"}) + | source: source_for_output({:terminal_text, "Printed output", %{chunk: false}}) }, %{ Livebook.Notebook.Cell.new(:code) - | source: source_for_output({:plain_text, "Custom text"}) + | source: source_for_output({:plain_text, "Custom text", %{chunk: false}}) } ] } @@ -121,7 +121,7 @@ defmodule LivebookWeb.AppSessionLiveTest do | cells: [ %{ Livebook.Notebook.Cell.new(:code) - | source: source_for_output({:stdout, "Printed output"}) + | source: source_for_output({:terminal_text, "Printed output", %{chunk: false}}) }, %{ Livebook.Notebook.Cell.new(:code) diff --git a/test/livebook_web/live/session_live_test.exs b/test/livebook_web/live/session_live_test.exs index 064c3fa92..909608c5b 100644 --- a/test/livebook_web/live/session_live_test.exs +++ b/test/livebook_web/live/session_live_test.exs @@ -176,8 +176,8 @@ defmodule LivebookWeb.SessionLiveTest do |> render_hook("queue_cell_evaluation", %{"cell_id" => cell_id}) assert_receive {:operation, - {:add_cell_evaluation_response, _, ^cell_id, {:text, "\e[32m\"true\"\e[0m"}, - _}} + {:add_cell_evaluation_response, _, ^cell_id, + {:terminal_text, "\e[32m\"true\"\e[0m", %{chunk: false}}, _}} end test "cancelling cell evaluation", %{conn: conn, session: session} do @@ -627,7 +627,7 @@ defmodule LivebookWeb.SessionLiveTest do end describe "outputs" do - test "stdout output update", %{conn: conn, session: session} do + test "chunked text output update", %{conn: conn, session: session} do Session.subscribe(session.id) evaluate_setup(session.pid) @@ -636,14 +636,21 @@ defmodule LivebookWeb.SessionLiveTest do Session.queue_cell_evaluation(session.pid, cell_id) - send(session.pid, {:runtime_evaluation_output, cell_id, {:stdout, "line 1\n"}}) + send( + session.pid, + {:runtime_evaluation_output, cell_id, {:terminal_text, "line 1\n", %{chunk: true}}} + ) {:ok, view, _} = live(conn, ~p"/sessions/#{session.id}") assert render(view) =~ "line 1" - send(session.pid, {:runtime_evaluation_output, cell_id, {:stdout, "line 2\n"}}) + send( + session.pid, + {:runtime_evaluation_output, cell_id, {:terminal_text, "line 2\n", %{chunk: true}}} + ) + wait_for_session_update(session.pid) - # Render once, so that stdout send_update is processed + # Render once, so that the send_update is processed _ = render(view) assert render(view) =~ "line 2" end @@ -660,7 +667,7 @@ defmodule LivebookWeb.SessionLiveTest do send( session.pid, {:runtime_evaluation_output, cell_id, - {:frame, [{:text, "In frame"}], %{ref: "1", type: :default}}} + {:frame, [{:terminal_text, "In frame", %{chunk: false}}], %{ref: "1", type: :default}}} ) {:ok, view, _} = live(conn, ~p"/sessions/#{session.id}") @@ -669,7 +676,8 @@ defmodule LivebookWeb.SessionLiveTest do send( session.pid, {:runtime_evaluation_output, cell_id, - {:frame, [{:text, "Updated frame"}], %{ref: "1", type: :replace}}} + {:frame, [{:terminal_text, "Updated frame", %{chunk: false}}], + %{ref: "1", type: :replace}}} ) wait_for_session_update(session.pid) @@ -696,11 +704,13 @@ defmodule LivebookWeb.SessionLiveTest do send( session.pid, - {:runtime_evaluation_output_to, client_id, cell_id, {:stdout, "line 1\n"}} + {:runtime_evaluation_output_to, client_id, cell_id, + {:terminal_text, "line 1\n", %{chunk: true}}} ) assert_receive {:operation, - {:add_cell_evaluation_output, _, ^cell_id, {:stdout, "line 1\n"}}} + {:add_cell_evaluation_output, _, ^cell_id, + {:terminal_text, "line 1\n", %{chunk: true}}}} {:ok, view, _} = live(conn, ~p"/sessions/#{session.id}") refute render(view) =~ "line 1" @@ -721,11 +731,13 @@ defmodule LivebookWeb.SessionLiveTest do send( session.pid, - {:runtime_evaluation_output_to_clients, cell_id, {:stdout, "line 1\n"}} + {:runtime_evaluation_output_to_clients, cell_id, + {:terminal_text, "line 1\n", %{chunk: false}}} ) assert_receive {:operation, - {:add_cell_evaluation_output, _, ^cell_id, {:stdout, "line 1\n"}}} + {:add_cell_evaluation_output, _, ^cell_id, + {:terminal_text, "line 1\n", %{chunk: false}}}} {:ok, view, _} = live(conn, ~p"/sessions/#{session.id}") refute render(view) =~ "line 1" @@ -1467,7 +1479,8 @@ defmodule LivebookWeb.SessionLiveTest do Session.queue_cell_evaluation(session.pid, cell_id) assert_receive {:operation, - {:add_cell_evaluation_response, _, ^cell_id, {:text, output}, _}} + {:add_cell_evaluation_response, _, ^cell_id, + {:terminal_text, output, %{chunk: false}}, _}} assert output == "\e[32m\"#{secret.value}\"\e[0m" end @@ -1514,7 +1527,8 @@ defmodule LivebookWeb.SessionLiveTest do Session.queue_cell_evaluation(session.pid, cell_id) assert_receive {:operation, - {:add_cell_evaluation_response, _, ^cell_id, {:text, output}, _}} + {:add_cell_evaluation_response, _, ^cell_id, + {:terminal_text, output, %{chunk: false}}, _}} assert output == "\e[32m\"#{secret.value}\"\e[0m" end @@ -1605,7 +1619,8 @@ defmodule LivebookWeb.SessionLiveTest do |> render_hook("queue_cell_evaluation", %{"cell_id" => cell_id}) assert_receive {:operation, - {:add_cell_evaluation_response, _, ^cell_id, {:text, "\e[35mnil\e[0m"}, _}} + {:add_cell_evaluation_response, _, ^cell_id, + {:terminal_text, "\e[35mnil\e[0m", %{chunk: false}}, _}} attrs = params_for(:env_var, name: "MY_AWESOME_ENV", value: "MyEnvVarValue") Settings.set_env_var(attrs) @@ -1616,7 +1631,7 @@ defmodule LivebookWeb.SessionLiveTest do assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, - {:text, "\e[32m\"MyEnvVarValue\"\e[0m"}, _}} + {:terminal_text, "\e[32m\"MyEnvVarValue\"\e[0m", %{chunk: false}}, _}} Settings.set_env_var(%{attrs | value: "OTHER_VALUE"}) @@ -1626,7 +1641,7 @@ defmodule LivebookWeb.SessionLiveTest do assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, - {:text, "\e[32m\"OTHER_VALUE\"\e[0m"}, _}} + {:terminal_text, "\e[32m\"OTHER_VALUE\"\e[0m", %{chunk: false}}, _}} Settings.unset_env_var("MY_AWESOME_ENV") @@ -1635,7 +1650,8 @@ defmodule LivebookWeb.SessionLiveTest do |> render_hook("queue_cell_evaluation", %{"cell_id" => cell_id}) assert_receive {:operation, - {:add_cell_evaluation_response, _, ^cell_id, {:text, "\e[35mnil\e[0m"}, _}} + {:add_cell_evaluation_response, _, ^cell_id, + {:terminal_text, "\e[35mnil\e[0m", %{chunk: false}}, _}} end @tag :tmp_dir @@ -1669,7 +1685,8 @@ defmodule LivebookWeb.SessionLiveTest do |> render_hook("queue_cell_evaluation", %{"cell_id" => cell_id}) assert_receive {:operation, - {:add_cell_evaluation_response, _, ^cell_id, {:text, output}, _}} + {:add_cell_evaluation_response, _, ^cell_id, + {:terminal_text, output, %{chunk: false}}, _}} assert output == "\e[32m\"#{String.replace(expected_path, "\\", "\\\\")}\"\e[0m" @@ -1680,7 +1697,8 @@ defmodule LivebookWeb.SessionLiveTest do |> render_hook("queue_cell_evaluation", %{"cell_id" => cell_id}) assert_receive {:operation, - {:add_cell_evaluation_response, _, ^cell_id, {:text, output}, _}} + {:add_cell_evaluation_response, _, ^cell_id, + {:terminal_text, output, %{chunk: false}}, _}} assert output == "\e[32m\"#{String.replace(initial_os_path, "\\", "\\\\")}\"\e[0m" end @@ -2009,7 +2027,12 @@ defmodule LivebookWeb.SessionLiveTest do {:ok, view, _} = live(conn, ~p"/sessions/#{session.id}") section_id = insert_section(session.pid) - insert_cell_with_output(session.pid, section_id, {:text, "Hello from the app!"}) + + insert_cell_with_output( + session.pid, + section_id, + {:terminal_text, "Hello from the app!", %{chunk: false}} + ) slug = Livebook.Utils.random_short_id()