mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-10-22 11:26:24 +08:00
Add support for Erlang code cells (#1892)
Co-authored-by: José Valim <jose.valim@gmail.com> Co-authored-by: Jonatan Kłosko <jonatanklosko@gmail.com>
This commit is contained in:
parent
14dd6d925f
commit
fb9193b8ab
36 changed files with 1033 additions and 436 deletions
|
@ -0,0 +1,43 @@
|
|||
// Adapted from https://github.com/erlang-ls/vscode/blob/0.0.39/language-configuration.json
|
||||
const ErlangLanguageConfiguration = {
|
||||
comments: {
|
||||
lineComment: "%",
|
||||
},
|
||||
brackets: [
|
||||
["(", ")"],
|
||||
["[", "]"],
|
||||
["{", "}"],
|
||||
],
|
||||
autoClosingPairs: [
|
||||
{ open: "(", close: ")" },
|
||||
{ open: "[", close: "]" },
|
||||
{ open: "{", close: "}" },
|
||||
{ open: "'", close: "'", notIn: ["string", "comment"] },
|
||||
{ open: '"', close: '"', notIn: ["string"] },
|
||||
{ open: '<<"', close: '">>', notIn: ["string"] },
|
||||
],
|
||||
surroundingPairs: [
|
||||
{ open: "(", close: ")" },
|
||||
{ open: "[", close: "]" },
|
||||
{ open: "{", close: "}" },
|
||||
{ open: "'", close: "'" },
|
||||
{ open: '"', close: '"' },
|
||||
],
|
||||
indentationRules: {
|
||||
// Indent if a line ends brackets, "->" or most keywords. Also if prefixed
|
||||
// with "||". This should work with most formatting models.
|
||||
// The ((?!%).)* is to ensure this doesn't match inside comments.
|
||||
increaseIndentPattern:
|
||||
/^((?!%).)*([{([]|->|after|begin|case|catch|fun|if|of|try|when|(\|\|.*))\s*$/,
|
||||
// Dedent after brackets, end or lone "->". The latter happens in a spec
|
||||
// with indented types, typically after "when". Only do this if it's _only_
|
||||
// preceded by whitespace.
|
||||
decreaseIndentPattern: /^\s*([)}]]|end|->\s*$)/,
|
||||
// Indent if after an incomplete map association operator, list
|
||||
// comprehension and type specifier. But only once, then return to the
|
||||
// previous indent.
|
||||
indentNextLinePattern: /^((?!%).)*(::|=>|:=|<-)\s*$/,
|
||||
},
|
||||
};
|
||||
|
||||
export default ErlangLanguageConfiguration;
|
|
@ -0,0 +1,156 @@
|
|||
const ErlangMonarchLanguage = {
|
||||
// Set defaultToken to invalid to see what you do not tokenize yet
|
||||
// defaultToken: 'invalid',
|
||||
|
||||
keywords: [
|
||||
"case",
|
||||
"if",
|
||||
"begin",
|
||||
"end",
|
||||
"when",
|
||||
"of",
|
||||
"fun",
|
||||
"maybe",
|
||||
"else",
|
||||
"try",
|
||||
"catch",
|
||||
"receive",
|
||||
"after",
|
||||
],
|
||||
|
||||
attributes: [
|
||||
"-module",
|
||||
"-record",
|
||||
"-export",
|
||||
"-spec",
|
||||
"-include",
|
||||
"-include_lib",
|
||||
"-export",
|
||||
"-undef",
|
||||
"-ifdef",
|
||||
"-ifndef",
|
||||
"-else",
|
||||
"-endif",
|
||||
"-if",
|
||||
"-elif",
|
||||
"-define",
|
||||
],
|
||||
|
||||
operators: [
|
||||
"=",
|
||||
"==",
|
||||
"=:=",
|
||||
"/=",
|
||||
"=/=",
|
||||
">",
|
||||
"<",
|
||||
"=<",
|
||||
">=",
|
||||
"+",
|
||||
"++",
|
||||
"-",
|
||||
"--",
|
||||
"*",
|
||||
"/",
|
||||
"!",
|
||||
"and",
|
||||
"or",
|
||||
"not",
|
||||
"xor",
|
||||
"andalso",
|
||||
"orelse",
|
||||
"bnot",
|
||||
"div",
|
||||
"rem",
|
||||
"band",
|
||||
"bor",
|
||||
"bxor",
|
||||
"bsl",
|
||||
"bsr",
|
||||
":=",
|
||||
"=>",
|
||||
"->",
|
||||
"?=",
|
||||
"<-",
|
||||
"||",
|
||||
],
|
||||
|
||||
builtins: ["error", "exit"],
|
||||
|
||||
brackets: [
|
||||
["(", ")", "delimiter.parenthesis"],
|
||||
["{", "}", "delimiter.curly"],
|
||||
["[", "]", "delimiter.square"],
|
||||
],
|
||||
|
||||
symbols: /[=><~&|+\-*\/%@#]+/,
|
||||
|
||||
escapes:
|
||||
/\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,
|
||||
|
||||
tokenizer: {
|
||||
root: [
|
||||
[
|
||||
/(-[a-z_]+)/,
|
||||
{
|
||||
cases: {
|
||||
"@attributes": "keyword",
|
||||
"@default": "identifier",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
[/(\?[a-zA-Z_0-9]+)/, "constant"],
|
||||
|
||||
[/[A-Z_][a-z0-9_]*/, "identifier"],
|
||||
[
|
||||
/[a-z_][\w\-']*/,
|
||||
{
|
||||
cases: {
|
||||
"@builtins": "predefined.identifier",
|
||||
"@keywords": "keyword",
|
||||
"@default": "identifier",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
// whitespace
|
||||
{ include: "@whitespace" },
|
||||
|
||||
// delimiters and operators
|
||||
[/[()\[\]\{\}]/, "@brackets"],
|
||||
[
|
||||
/@symbols/,
|
||||
{
|
||||
cases: {
|
||||
"@operators": "predefined.operator",
|
||||
"@default": "operator",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
// numbers
|
||||
[/\d*\.\d+([eE][\-+]?\d+)?/, "number.float"],
|
||||
[/16#[0-9a-fA-F]+/, "number.hex"],
|
||||
[/\d+/, "number"],
|
||||
|
||||
// strings
|
||||
[/"([^"\\]|\\.)*$/, "string.invalid"], // non-teminated string
|
||||
[/"/, { token: "string.quote", bracket: "@open", next: "@string" }],
|
||||
],
|
||||
|
||||
string: [
|
||||
[/[^\\"]+/, "string"],
|
||||
[/@escapes/, "string.escape"],
|
||||
[/\\./, "string.escape.invalid"],
|
||||
[/"/, { token: "string.quote", bracket: "@close", next: "@pop" }],
|
||||
],
|
||||
|
||||
whitespace: [
|
||||
[/[ \t\r\n]+/, "white"],
|
||||
[/%.*$/, "comment"],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default ErlangMonarchLanguage;
|
|
@ -99,6 +99,8 @@ import "monaco-editor/esm/vs/basic-languages/xml/xml.contribution";
|
|||
|
||||
import { CommandsRegistry } from "monaco-editor/esm/vs/platform/commands/common/commands";
|
||||
import ElixirOnTypeFormattingEditProvider from "./elixir/on_type_formatting_edit_provider";
|
||||
import ErlangMonarchLanguage from "./erlang/monarch_language";
|
||||
import ErlangLanguageConfiguration from "./erlang/language_configuration";
|
||||
import { theme, lightTheme } from "./theme";
|
||||
|
||||
import { PieceTreeTextBufferBuilder } from "monaco-editor/esm/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder";
|
||||
|
@ -165,6 +167,13 @@ monaco.languages.registerOnTypeFormattingEditProvider(
|
|||
ElixirOnTypeFormattingEditProvider
|
||||
);
|
||||
|
||||
monaco.languages.register({ id: "erlang" });
|
||||
monaco.languages.setMonarchTokensProvider("erlang", ErlangMonarchLanguage);
|
||||
monaco.languages.setLanguageConfiguration(
|
||||
"erlang",
|
||||
ErlangLanguageConfiguration
|
||||
);
|
||||
|
||||
// Define custom theme
|
||||
monaco.editor.defineTheme("default", theme);
|
||||
monaco.editor.defineTheme("light", lightTheme);
|
||||
|
|
|
@ -73,7 +73,7 @@ defmodule Livebook.LiveMarkdown.Export do
|
|||
end
|
||||
|
||||
defp notebook_metadata(notebook) do
|
||||
keys = [:persist_outputs, :autosave_interval_s, :hub_id]
|
||||
keys = [:persist_outputs, :autosave_interval_s, :default_language, :hub_id]
|
||||
metadata = put_unless_default(%{}, Map.take(notebook, keys), Map.take(Notebook.new(), keys))
|
||||
|
||||
app_settings_metadata = app_settings_metadata(notebook.app_settings)
|
||||
|
@ -155,7 +155,7 @@ defmodule Livebook.LiveMarkdown.Export do
|
|||
metadata = cell_metadata(cell)
|
||||
|
||||
cell =
|
||||
[delimiter, "elixir\n", code, "\n", delimiter]
|
||||
[delimiter, Atom.to_string(cell.language), "\n", code, "\n", delimiter]
|
||||
|> prepend_metadata(metadata)
|
||||
|
||||
if outputs == [] do
|
||||
|
@ -252,10 +252,10 @@ defmodule Livebook.LiveMarkdown.Export do
|
|||
defp encode_js_data(data) when is_binary(data), do: {:ok, data}
|
||||
defp encode_js_data(data), do: data |> ensure_order() |> Jason.encode()
|
||||
|
||||
defp get_code_cell_code(%{source: source, disable_formatting: true}),
|
||||
do: source
|
||||
defp get_code_cell_code(%{source: source, language: :elixir, disable_formatting: false}),
|
||||
do: format_elixir_code(source)
|
||||
|
||||
defp get_code_cell_code(%{source: source}), do: format_code(source)
|
||||
defp get_code_cell_code(%{source: source}), do: source
|
||||
|
||||
defp render_metadata(metadata) do
|
||||
metadata_json = metadata |> ensure_order() |> Jason.encode!()
|
||||
|
@ -302,7 +302,7 @@ defmodule Livebook.LiveMarkdown.Export do
|
|||
end)
|
||||
end
|
||||
|
||||
defp format_code(code) do
|
||||
defp format_elixir_code(code) do
|
||||
try do
|
||||
Code.format_string!(code)
|
||||
rescue
|
||||
|
|
|
@ -152,11 +152,13 @@ defmodule Livebook.LiveMarkdown.Import do
|
|||
end
|
||||
|
||||
defp group_elements(
|
||||
[{"pre", _, [{"code", [{"class", "elixir"}], [source], %{}}], %{}} | ast],
|
||||
[{"pre", _, [{"code", [{"class", language}], [source], %{}}], %{}} | ast],
|
||||
elems
|
||||
) do
|
||||
)
|
||||
when language in ["elixir", "erlang"] do
|
||||
{outputs, ast} = take_outputs(ast, [])
|
||||
group_elements(ast, [{:cell, :code, source, outputs} | elems])
|
||||
language = String.to_atom(language)
|
||||
group_elements(ast, [{:cell, :code, language, source, outputs} | elems])
|
||||
end
|
||||
|
||||
defp group_elements([ast_node | ast], [{:cell, :markdown, md_ast} | rest]) do
|
||||
|
@ -226,7 +228,7 @@ defmodule Livebook.LiveMarkdown.Import do
|
|||
end
|
||||
|
||||
defp build_notebook(
|
||||
[{:cell, :code, source, outputs}, {:cell, :smart, data} | elems],
|
||||
[{:cell, :code, :elixir, source, outputs}, {:cell, :smart, data} | elems],
|
||||
cells,
|
||||
sections,
|
||||
messages,
|
||||
|
@ -249,7 +251,7 @@ defmodule Livebook.LiveMarkdown.Import do
|
|||
end
|
||||
|
||||
defp build_notebook(
|
||||
[{:cell, :code, source, outputs} | elems],
|
||||
[{:cell, :code, language, source, outputs} | elems],
|
||||
cells,
|
||||
sections,
|
||||
messages,
|
||||
|
@ -258,7 +260,11 @@ defmodule Livebook.LiveMarkdown.Import do
|
|||
{metadata, elems} = grab_metadata(elems)
|
||||
attrs = cell_metadata_to_attrs(:code, metadata)
|
||||
{outputs, output_counter} = Notebook.index_outputs(outputs, output_counter)
|
||||
cell = %{Notebook.Cell.new(:code) | source: source, outputs: outputs} |> Map.merge(attrs)
|
||||
|
||||
cell =
|
||||
%{Notebook.Cell.new(:code) | source: source, language: language, outputs: outputs}
|
||||
|> Map.merge(attrs)
|
||||
|
||||
build_notebook(elems, [cell | cells], sections, messages, output_counter)
|
||||
end
|
||||
|
||||
|
@ -378,6 +384,11 @@ defmodule Livebook.LiveMarkdown.Import do
|
|||
{"autosave_interval_s", autosave_interval_s}, {attrs, messages} ->
|
||||
{Map.put(attrs, :autosave_interval_s, autosave_interval_s), messages}
|
||||
|
||||
{"default_language", default_language}, {attrs, messages}
|
||||
when default_language in ["elixir", "erlang"] ->
|
||||
default_language = String.to_atom(default_language)
|
||||
{Map.put(attrs, :default_language, default_language), messages}
|
||||
|
||||
{"hub_id", hub_id}, {attrs, messages} ->
|
||||
if Livebook.Hubs.hub_exists?(hub_id) do
|
||||
{Map.put(attrs, :hub_id, hub_id), messages}
|
||||
|
|
|
@ -21,6 +21,7 @@ defmodule Livebook.Notebook do
|
|||
:leading_comments,
|
||||
:persist_outputs,
|
||||
:autosave_interval_s,
|
||||
:default_language,
|
||||
:output_counter,
|
||||
:app_settings,
|
||||
:hub_id,
|
||||
|
@ -39,6 +40,7 @@ defmodule Livebook.Notebook do
|
|||
leading_comments: list(list(line :: String.t())),
|
||||
persist_outputs: boolean(),
|
||||
autosave_interval_s: non_neg_integer() | nil,
|
||||
default_language: :elixir | :erlang,
|
||||
output_counter: non_neg_integer(),
|
||||
app_settings: AppSettings.t(),
|
||||
hub_id: String.t(),
|
||||
|
@ -60,6 +62,7 @@ defmodule Livebook.Notebook do
|
|||
leading_comments: [],
|
||||
persist_outputs: default_persist_outputs(),
|
||||
autosave_interval_s: default_autosave_interval_s(),
|
||||
default_language: :elixir,
|
||||
output_counter: 0,
|
||||
app_settings: AppSettings.new(),
|
||||
hub_id: Livebook.Hubs.Personal.id(),
|
||||
|
|
|
@ -10,6 +10,7 @@ defmodule Livebook.Notebook.Cell.Code do
|
|||
:id,
|
||||
:source,
|
||||
:outputs,
|
||||
:language,
|
||||
:disable_formatting,
|
||||
:reevaluate_automatically,
|
||||
:continue_on_error
|
||||
|
@ -22,6 +23,7 @@ defmodule Livebook.Notebook.Cell.Code do
|
|||
id: Cell.id(),
|
||||
source: String.t() | :__pruned__,
|
||||
outputs: list(Cell.indexed_output()),
|
||||
language: :elixir | :erlang,
|
||||
disable_formatting: boolean(),
|
||||
reevaluate_automatically: boolean(),
|
||||
continue_on_error: boolean()
|
||||
|
@ -36,6 +38,7 @@ defmodule Livebook.Notebook.Cell.Code do
|
|||
id: Utils.random_id(),
|
||||
source: "",
|
||||
outputs: [],
|
||||
language: :elixir,
|
||||
disable_formatting: false,
|
||||
reevaluate_automatically: false,
|
||||
continue_on_error: false
|
||||
|
|
|
@ -56,7 +56,7 @@ defmodule Livebook.Notebook.Export.Elixir do
|
|||
|> Enum.map_intersperse("\n", &comment_out/1)
|
||||
end
|
||||
|
||||
defp render_cell(%Cell.Code{} = cell, section) do
|
||||
defp render_cell(%Cell.Code{language: :elixir} = cell, section) do
|
||||
code = get_code_cell_code(cell)
|
||||
|
||||
if section.parent_id do
|
||||
|
@ -69,6 +69,15 @@ defmodule Livebook.Notebook.Export.Elixir do
|
|||
end
|
||||
end
|
||||
|
||||
defp render_cell(%Cell.Code{} = cell, _section) do
|
||||
code = cell.source
|
||||
|
||||
code
|
||||
|> IO.iodata_to_binary()
|
||||
|> String.split("\n")
|
||||
|> Enum.map_intersperse("\n", &comment_out/1)
|
||||
end
|
||||
|
||||
defp render_cell(%Cell.Smart{} = cell, ctx) do
|
||||
render_cell(%{Cell.Code.new() | source: cell.source}, ctx)
|
||||
end
|
||||
|
|
|
@ -438,8 +438,8 @@ defprotocol Livebook.Runtime do
|
|||
to be evaluated, if applicable
|
||||
|
||||
"""
|
||||
@spec evaluate_code(t(), String.t(), locator(), parent_locators(), keyword()) :: :ok
|
||||
def evaluate_code(runtime, code, locator, parent_locators, opts \\ [])
|
||||
@spec evaluate_code(t(), atom(), String.t(), locator(), parent_locators(), keyword()) :: :ok
|
||||
def evaluate_code(runtime, language, code, locator, parent_locators, opts \\ [])
|
||||
|
||||
@doc """
|
||||
Disposes of an evaluation identified by the given locator.
|
||||
|
|
|
@ -108,8 +108,15 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Attached do
|
|||
Livebook.Runtime.Attached.new(runtime.node, runtime.cookie)
|
||||
end
|
||||
|
||||
def evaluate_code(runtime, code, locator, parent_locators, opts \\ []) do
|
||||
RuntimeServer.evaluate_code(runtime.server_pid, code, locator, parent_locators, opts)
|
||||
def evaluate_code(runtime, language, code, locator, parent_locators, opts \\ []) do
|
||||
RuntimeServer.evaluate_code(
|
||||
runtime.server_pid,
|
||||
language,
|
||||
code,
|
||||
locator,
|
||||
parent_locators,
|
||||
opts
|
||||
)
|
||||
end
|
||||
|
||||
def forget_evaluation(runtime, locator) do
|
||||
|
|
|
@ -110,8 +110,15 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.ElixirStandalone do
|
|||
Livebook.Runtime.ElixirStandalone.new()
|
||||
end
|
||||
|
||||
def evaluate_code(runtime, code, locator, parent_locators, opts \\ []) do
|
||||
RuntimeServer.evaluate_code(runtime.server_pid, code, locator, parent_locators, opts)
|
||||
def evaluate_code(runtime, language, code, locator, parent_locators, opts \\ []) do
|
||||
RuntimeServer.evaluate_code(
|
||||
runtime.server_pid,
|
||||
language,
|
||||
code,
|
||||
locator,
|
||||
parent_locators,
|
||||
opts
|
||||
)
|
||||
end
|
||||
|
||||
def forget_evaluation(runtime, locator) do
|
||||
|
|
|
@ -76,8 +76,15 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Embedded do
|
|||
Livebook.Runtime.Embedded.new()
|
||||
end
|
||||
|
||||
def evaluate_code(runtime, code, locator, parent_locators, opts \\ []) do
|
||||
RuntimeServer.evaluate_code(runtime.server_pid, code, locator, parent_locators, opts)
|
||||
def evaluate_code(runtime, language, code, locator, parent_locators, opts \\ []) do
|
||||
RuntimeServer.evaluate_code(
|
||||
runtime.server_pid,
|
||||
language,
|
||||
code,
|
||||
locator,
|
||||
parent_locators,
|
||||
opts
|
||||
)
|
||||
end
|
||||
|
||||
def forget_evaluation(runtime, locator) do
|
||||
|
|
|
@ -26,7 +26,7 @@ defmodule Livebook.Runtime.ErlDist do
|
|||
Livebook.Runtime.Evaluator.IOProxy,
|
||||
Livebook.Runtime.Evaluator.Tracer,
|
||||
Livebook.Runtime.Evaluator.ObjectTracker,
|
||||
Livebook.Runtime.Evaluator.DefaultFormatter,
|
||||
Livebook.Runtime.Evaluator.Formatter,
|
||||
Livebook.Runtime.Evaluator.Doctests,
|
||||
Livebook.Intellisense,
|
||||
Livebook.Intellisense.Docs,
|
||||
|
|
|
@ -22,8 +22,6 @@ defmodule Livebook.Runtime.ErlDist.EvaluatorSupervisor do
|
|||
"""
|
||||
@spec start_evaluator(pid(), keyword()) :: {:ok, Evaluator.t()} | {:error, any()}
|
||||
def start_evaluator(supervisor, opts) do
|
||||
opts = Keyword.put_new(opts, :formatter, Evaluator.DefaultFormatter)
|
||||
|
||||
case DynamicSupervisor.start_child(supervisor, {Evaluator, opts}) do
|
||||
{:ok, _pid, evaluator} -> {:ok, evaluator}
|
||||
{:error, reason} -> {:error, reason}
|
||||
|
|
|
@ -114,7 +114,7 @@ defmodule Livebook.Runtime.ErlDist.NodeManager do
|
|||
Application.put_env(
|
||||
:elixir,
|
||||
:ansi_syntax_colors,
|
||||
Livebook.Runtime.Evaluator.DefaultFormatter.syntax_colors()
|
||||
Livebook.Runtime.Evaluator.Formatter.syntax_colors()
|
||||
)
|
||||
|
||||
tmp_dir = make_tmp_dir()
|
||||
|
|
|
@ -88,13 +88,14 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
|
|||
"""
|
||||
@spec evaluate_code(
|
||||
pid(),
|
||||
:elixir | :erlang,
|
||||
String.t(),
|
||||
Runtime.locator(),
|
||||
Runtime.parent_locators(),
|
||||
keyword()
|
||||
) :: :ok
|
||||
def evaluate_code(pid, code, locator, parent_locators, opts \\ []) do
|
||||
GenServer.cast(pid, {:evaluate_code, code, locator, parent_locators, opts})
|
||||
def evaluate_code(pid, language, code, locator, parent_locators, opts \\ []) do
|
||||
GenServer.cast(pid, {:evaluate_code, language, code, locator, parent_locators, opts})
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -433,7 +434,8 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
|
|||
end
|
||||
|
||||
def handle_cast(
|
||||
{:evaluate_code, code, {container_ref, evaluation_ref} = locator, parent_locators, opts},
|
||||
{:evaluate_code, language, code, {container_ref, evaluation_ref} = locator,
|
||||
parent_locators, opts},
|
||||
state
|
||||
) do
|
||||
state = ensure_evaluator(state, container_ref)
|
||||
|
@ -460,6 +462,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
|
|||
|
||||
Evaluator.evaluate_code(
|
||||
state.evaluators[container_ref],
|
||||
language,
|
||||
code,
|
||||
evaluation_ref,
|
||||
parent_evaluation_refs,
|
||||
|
|
|
@ -26,7 +26,6 @@ defmodule Livebook.Runtime.Evaluator do
|
|||
|
||||
@type state :: %{
|
||||
evaluator_ref: reference(),
|
||||
formatter: module(),
|
||||
io_proxy: pid(),
|
||||
io_proxy_monitor: reference(),
|
||||
send_to: pid(),
|
||||
|
@ -82,10 +81,6 @@ defmodule Livebook.Runtime.Evaluator do
|
|||
* `:runtime_broadcast_to` - the process to send runtime broadcast
|
||||
events to. Defaults to the value of `:send_to`
|
||||
|
||||
* `:formatter` - a module implementing the `Livebook.Runtime.Evaluator.Formatter`
|
||||
behaviour, used for transforming evaluation result before sending
|
||||
it to the client. Defaults to identity
|
||||
|
||||
* `:ebin_path` - a directory to write modules bytecode into. When
|
||||
not specified, modules are not written to disk
|
||||
|
||||
|
@ -137,8 +132,8 @@ defmodule Livebook.Runtime.Evaluator do
|
|||
of previous evaluations, in which case the corresponding context is
|
||||
used as the entry point for evaluation.
|
||||
|
||||
The evaluation result is transformed with the configured
|
||||
formatter send to the configured client (see `start_link/1`).
|
||||
The evaluation result is formatted into an output and sent to the
|
||||
configured client (see `start_link/1`) together with metadata.
|
||||
|
||||
See `Livebook.Runtime.evaluate_code/5` for the messages format
|
||||
and the list of available options.
|
||||
|
@ -150,9 +145,9 @@ defmodule Livebook.Runtime.Evaluator do
|
|||
as an argument
|
||||
|
||||
"""
|
||||
@spec evaluate_code(t(), String.t(), ref(), list(ref()), keyword()) :: :ok
|
||||
def evaluate_code(evaluator, code, ref, parent_refs, opts \\ []) do
|
||||
cast(evaluator, {:evaluate_code, code, ref, parent_refs, opts})
|
||||
@spec evaluate_code(t(), :elixir | :erlang, ref(), list(ref()), keyword()) :: :ok
|
||||
def evaluate_code(evaluator, language, code, ref, parent_refs, opts \\ []) do
|
||||
cast(evaluator, {:evaluate_code, language, code, ref, parent_refs, opts})
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -272,7 +267,6 @@ defmodule Livebook.Runtime.Evaluator do
|
|||
send_to = Keyword.fetch!(opts, :send_to)
|
||||
runtime_broadcast_to = Keyword.get(opts, :runtime_broadcast_to, send_to)
|
||||
object_tracker = Keyword.fetch!(opts, :object_tracker)
|
||||
formatter = Keyword.get(opts, :formatter, Evaluator.IdentityFormatter)
|
||||
ebin_path = Keyword.get(opts, :ebin_path)
|
||||
io_proxy_registry = Keyword.get(opts, :io_proxy_registry)
|
||||
|
||||
|
@ -309,7 +303,6 @@ defmodule Livebook.Runtime.Evaluator do
|
|||
|
||||
state = %{
|
||||
evaluator_ref: evaluator_ref,
|
||||
formatter: formatter,
|
||||
io_proxy: io_proxy,
|
||||
io_proxy_monitor: io_proxy_monitor,
|
||||
send_to: send_to,
|
||||
|
@ -348,8 +341,8 @@ defmodule Livebook.Runtime.Evaluator do
|
|||
%{id: random_id(), binding: [], env: env, pdict: %{}}
|
||||
end
|
||||
|
||||
defp handle_cast({:evaluate_code, code, ref, parent_refs, opts}, state) do
|
||||
do_evaluate_code(code, ref, parent_refs, opts, state)
|
||||
defp handle_cast({:evaluate_code, language, code, ref, parent_refs, opts}, state) do
|
||||
do_evaluate_code(language, code, ref, parent_refs, opts, state)
|
||||
end
|
||||
|
||||
defp handle_cast({:forget_evaluation, ref}, state) do
|
||||
|
@ -401,7 +394,7 @@ defmodule Livebook.Runtime.Evaluator do
|
|||
{:reply, result, state}
|
||||
end
|
||||
|
||||
defp do_evaluate_code(code, ref, parent_refs, opts, state) do
|
||||
defp do_evaluate_code(language, code, ref, parent_refs, opts, state) do
|
||||
{old_context, state} = pop_in(state.contexts[ref])
|
||||
|
||||
if old_context do
|
||||
|
@ -413,10 +406,10 @@ defmodule Livebook.Runtime.Evaluator do
|
|||
# We remove the old context from state and jump to a tail-recursive
|
||||
# function. This way we are sure there is no reference to the old
|
||||
# state and we can garbage collect the old context before the evaluation
|
||||
continue_do_evaluate_code(code, ref, parent_refs, opts, state)
|
||||
continue_do_evaluate_code(language, code, ref, parent_refs, opts, state)
|
||||
end
|
||||
|
||||
defp continue_do_evaluate_code(code, ref, parent_refs, opts, state) do
|
||||
defp continue_do_evaluate_code(language, code, ref, parent_refs, opts, state) do
|
||||
:erlang.garbage_collect(self())
|
||||
|
||||
Evaluator.ObjectTracker.remove_reference_sync(state.object_tracker, {self(), ref})
|
||||
|
@ -430,7 +423,7 @@ defmodule Livebook.Runtime.Evaluator do
|
|||
set_pdict(context, state.ignored_pdict_keys)
|
||||
|
||||
start_time = System.monotonic_time()
|
||||
{eval_result, code_markers} = eval(code, context.binding, context.env)
|
||||
{eval_result, code_markers} = eval(language, code, context.binding, context.env)
|
||||
evaluation_time_ms = time_diff_ms(start_time)
|
||||
|
||||
%{tracer_info: tracer_info} = Evaluator.IOProxy.after_evaluation(state.io_proxy)
|
||||
|
@ -474,15 +467,11 @@ defmodule Livebook.Runtime.Evaluator do
|
|||
|
||||
state = put_context(state, ref, new_context)
|
||||
|
||||
output = state.formatter.format_result(result)
|
||||
output = Evaluator.Formatter.format_result(result, language)
|
||||
|
||||
metadata = %{
|
||||
errored: elem(result, 0) == :error,
|
||||
interrupted:
|
||||
match?(
|
||||
{:error, _kind, error, _stacktrace} when is_struct(error, Kino.InterruptError),
|
||||
result
|
||||
),
|
||||
errored: error_result?(result),
|
||||
interrupted: interrupt_result?(result),
|
||||
evaluation_time_ms: evaluation_time_ms,
|
||||
memory_usage: memory(),
|
||||
code_markers: code_markers,
|
||||
|
@ -501,6 +490,15 @@ defmodule Livebook.Runtime.Evaluator do
|
|||
{:noreply, state}
|
||||
end
|
||||
|
||||
defp error_result?(result) when elem(result, 0) == :error, do: true
|
||||
defp error_result?(_result), do: false
|
||||
|
||||
defp interrupt_result?({:error, _kind, error, _stacktrace})
|
||||
when is_struct(error, Kino.InterruptError),
|
||||
do: true
|
||||
|
||||
defp interrupt_result?(_result), do: false
|
||||
|
||||
defp do_forget_evaluation(ref, state) do
|
||||
{context, state} = pop_context(state, ref)
|
||||
|
||||
|
@ -614,7 +612,7 @@ defmodule Livebook.Runtime.Evaluator do
|
|||
|> Map.update!(:context_modules, &(&1 ++ prev_env.context_modules))
|
||||
end
|
||||
|
||||
defp eval(code, binding, env) do
|
||||
defp eval(:elixir, code, binding, env) do
|
||||
{{result, extra_diagnostics}, diagnostics} =
|
||||
with_diagnostics([log: true], fn ->
|
||||
try do
|
||||
|
@ -627,7 +625,7 @@ defmodule Livebook.Runtime.Evaluator do
|
|||
{:ok, value, binding, env}
|
||||
catch
|
||||
kind, error ->
|
||||
stacktrace = prune_stacktrace(__STACKTRACE__)
|
||||
stacktrace = prune_stacktrace(:elixir_eval, __STACKTRACE__)
|
||||
{:error, kind, error, stacktrace}
|
||||
end
|
||||
catch
|
||||
|
@ -678,6 +676,96 @@ defmodule Livebook.Runtime.Evaluator do
|
|||
{result, code_markers}
|
||||
end
|
||||
|
||||
defp eval(:erlang, code, binding, env) do
|
||||
try do
|
||||
erl_binding =
|
||||
Enum.reduce(binding, %{}, fn {name, value}, erl_binding ->
|
||||
:erl_eval.add_binding(elixir_to_erlang_var(name), value, erl_binding)
|
||||
end)
|
||||
|
||||
with {:ok, tokens, _} <- :erl_scan.string(String.to_charlist(code)),
|
||||
{:ok, parsed} <- :erl_parse.parse_exprs(tokens),
|
||||
{:value, result, new_erl_binding} <- :erl_eval.exprs(parsed, erl_binding) do
|
||||
# Simple heuristic to detect the used variables. We look at
|
||||
# the tokens and assume all var tokens are used variables.
|
||||
# This will not handle shadowing of variables in fun definitions
|
||||
# and will only work well enough for expressions, not for modules.
|
||||
used_vars =
|
||||
for {:var, _anno, name} <- tokens,
|
||||
do: {erlang_to_elixir_var(name), nil},
|
||||
into: MapSet.new(),
|
||||
uniq: true
|
||||
|
||||
# Note that for Elixir we evaluate with :prune_binding, here
|
||||
# replicate the same behaviour for binding and env
|
||||
|
||||
binding =
|
||||
new_erl_binding
|
||||
|> Map.drop(Map.keys(erl_binding))
|
||||
|> Enum.map(fn {name, value} ->
|
||||
{erlang_to_elixir_var(name), value}
|
||||
end)
|
||||
|
||||
env =
|
||||
update_in(env.versioned_vars, fn versioned_vars ->
|
||||
versioned_vars
|
||||
|> Map.filter(fn {var, _} -> MapSet.member?(used_vars, var) end)
|
||||
|> Map.merge(
|
||||
binding
|
||||
|> Enum.with_index(Kernel.map_size(versioned_vars) + 1)
|
||||
|> Map.new(fn {{name, _value}, version} -> {{name, nil}, version} end)
|
||||
)
|
||||
end)
|
||||
|
||||
{{:ok, result, binding, env}, []}
|
||||
else
|
||||
# Tokenizer error
|
||||
{:error, err, location} ->
|
||||
code_marker = %{
|
||||
line: :erl_anno.line(location),
|
||||
severity: :error,
|
||||
description: "Tokenizer #{err}"
|
||||
}
|
||||
|
||||
{{:error, :error, {:token, err}, []}, filter_erlang_code_markers([code_marker])}
|
||||
|
||||
# Parser error
|
||||
{:error, {location, _module, err}} ->
|
||||
err = :erlang.list_to_binary(err)
|
||||
|
||||
code_marker = %{
|
||||
line: :erl_anno.line(location),
|
||||
severity: :error,
|
||||
description: "Parser #{err}"
|
||||
}
|
||||
|
||||
{{:error, :error, err, []}, filter_erlang_code_markers([code_marker])}
|
||||
end
|
||||
catch
|
||||
kind, error ->
|
||||
stacktrace = prune_stacktrace(:erl_eval, __STACKTRACE__)
|
||||
{{:error, kind, error, stacktrace}, []}
|
||||
end
|
||||
end
|
||||
|
||||
defp elixir_to_erlang_var(name) do
|
||||
name
|
||||
|> :erlang.atom_to_binary()
|
||||
|> Macro.camelize()
|
||||
|> :erlang.binary_to_atom()
|
||||
end
|
||||
|
||||
defp erlang_to_elixir_var(name) do
|
||||
name
|
||||
|> :erlang.atom_to_binary()
|
||||
|> Macro.underscore()
|
||||
|> :erlang.binary_to_atom()
|
||||
end
|
||||
|
||||
defp filter_erlang_code_markers(code_markers) do
|
||||
Enum.reject(code_markers, &(&1.line == 0))
|
||||
end
|
||||
|
||||
# TODO: remove once we require Elixir v1.15
|
||||
if Code.ensure_loaded?(Code) and function_exported?(Code, :with_diagnostics, 2) do
|
||||
defp with_diagnostics(opts, fun) do
|
||||
|
@ -826,15 +914,16 @@ defmodule Livebook.Runtime.Evaluator do
|
|||
do: var
|
||||
end
|
||||
|
||||
defp prune_stacktrace([{Livebook.Runtime.Evaluator.Tracer, _fun, _arity, _meta} | _]), do: []
|
||||
defp prune_stacktrace(_module, [{Livebook.Runtime.Evaluator.Tracer, _fun, _arity, _meta} | _]),
|
||||
do: []
|
||||
|
||||
# Adapted from https://github.com/elixir-lang/elixir/blob/1c1654c88adfdbef38ff07fc30f6fbd34a542c07/lib/iex/lib/iex/evaluator.ex#L355-L372
|
||||
# TODO: Remove else branch once we depend on the versions below
|
||||
if System.otp_release() >= "25" do
|
||||
defp prune_stacktrace(stack) do
|
||||
defp prune_stacktrace(module, stack) do
|
||||
stack
|
||||
|> Enum.reverse()
|
||||
|> Enum.drop_while(&(elem(&1, 0) != :elixir_eval))
|
||||
|> Enum.drop_while(&(elem(&1, 0) != module))
|
||||
|> Enum.reverse()
|
||||
|> case do
|
||||
[] -> stack
|
||||
|
@ -846,7 +935,7 @@ defmodule Livebook.Runtime.Evaluator do
|
|||
[:elixir_clauses, :elixir_lexical, :elixir_def, :elixir_map] ++
|
||||
[:elixir_erl, :elixir_erl_clauses, :elixir_erl_pass]
|
||||
|
||||
defp prune_stacktrace(stacktrace) do
|
||||
defp prune_stacktrace(_, stacktrace) do
|
||||
# The order in which each drop_while is listed is important.
|
||||
# For example, the user may call Code.eval_string/2 in their
|
||||
# code and if there is an error we should not remove erl_eval
|
||||
|
|
|
@ -1,146 +0,0 @@
|
|||
defmodule Livebook.Runtime.Evaluator.DefaultFormatter do
|
||||
@moduledoc false
|
||||
|
||||
# The formatter used by Livebook for rendering the results.
|
||||
#
|
||||
# See `Livebook.Runtime.output/0` for available output formats.
|
||||
|
||||
@behaviour Livebook.Runtime.Evaluator.Formatter
|
||||
|
||||
require Logger
|
||||
|
||||
@impl true
|
||||
def format_result({:ok, :"do not show this result in output"}) do
|
||||
# Functions in the `IEx.Helpers` module return this specific value
|
||||
# to indicate no result should be printed in the iex shell,
|
||||
# so we respect that as well.
|
||||
:ignored
|
||||
end
|
||||
|
||||
def format_result({:ok, {:module, _, _, _} = value}) do
|
||||
to_inspect_output(value, limit: 10)
|
||||
end
|
||||
|
||||
def format_result({:ok, value}) do
|
||||
to_output(value)
|
||||
end
|
||||
|
||||
def format_result({:error, kind, error, stacktrace}) do
|
||||
formatted = format_error(kind, error, stacktrace)
|
||||
{:error, formatted, error_type(error)}
|
||||
end
|
||||
|
||||
@compile {:no_warn_undefined, {Kino.Render, :to_livebook, 1}}
|
||||
|
||||
defp to_output(value) do
|
||||
# Kino is a "client side" extension for Livebook that may be
|
||||
# installed into the runtime node. If it is installed we use
|
||||
# its more precise output rendering rules.
|
||||
if Code.ensure_loaded?(Kino.Render) do
|
||||
try do
|
||||
Kino.Render.to_livebook(value)
|
||||
catch
|
||||
kind, error ->
|
||||
formatted = format_error(kind, error, __STACKTRACE__)
|
||||
Logger.error(formatted)
|
||||
to_inspect_output(value)
|
||||
end
|
||||
else
|
||||
to_inspect_output(value)
|
||||
end
|
||||
end
|
||||
|
||||
defp to_inspect_output(value, opts \\ []) do
|
||||
try do
|
||||
inspected = inspect(value, inspect_opts(opts))
|
||||
{:text, inspected}
|
||||
catch
|
||||
kind, error ->
|
||||
formatted = format_error(kind, error, __STACKTRACE__)
|
||||
{:error, formatted}
|
||||
end
|
||||
end
|
||||
|
||||
def inspect_opts(opts \\ []) do
|
||||
default_opts = [pretty: true, width: 100, syntax_colors: syntax_colors()]
|
||||
Keyword.merge(default_opts, opts)
|
||||
end
|
||||
|
||||
def syntax_colors() do
|
||||
# Note: we intentionally don't specify colors
|
||||
# for `:binary`, `:list`, `:map` and `:tuple`
|
||||
# and rely on these using the default text color.
|
||||
# This way we avoid a bunch of HTML tags for coloring commas, etc.
|
||||
[
|
||||
atom: :blue,
|
||||
# binary: :light_black,
|
||||
boolean: :magenta,
|
||||
# list: :light_black,
|
||||
# map: :light_black,
|
||||
number: :blue,
|
||||
nil: :magenta,
|
||||
regex: :red,
|
||||
string: :green,
|
||||
# tuple: :light_black,
|
||||
reset: :reset
|
||||
]
|
||||
end
|
||||
|
||||
defp format_error(kind, error, stacktrace) do
|
||||
{blamed, stacktrace} =
|
||||
case error do
|
||||
%CompileError{description: "cannot compile file (errors have been logged)" <> _, line: 0} ->
|
||||
{%CompileError{description: "cannot compile cell (errors have been logged)"}, []}
|
||||
|
||||
_ ->
|
||||
Exception.blame(kind, error, stacktrace)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
defp error_type(%System.EnvError{env: "LB_" <> secret_name}),
|
||||
do: {:missing_secret, secret_name}
|
||||
|
||||
defp error_type(error) when is_struct(error, Kino.InterruptError),
|
||||
do: {:interrupt, error.variant, error.message}
|
||||
|
||||
defp error_type(_), do: :other
|
||||
end
|
|
@ -261,7 +261,7 @@ defmodule Livebook.Runtime.Evaluator.Doctests do
|
|||
case blamed do
|
||||
%FunctionClauseError{} ->
|
||||
banner = Exception.format_banner(kind, reason, stacktrace)
|
||||
inspect_opts = Livebook.Runtime.Evaluator.DefaultFormatter.inspect_opts()
|
||||
inspect_opts = Livebook.Runtime.Evaluator.Formatter.inspect_opts()
|
||||
blame = FunctionClauseError.blame(blamed, &inspect(&1, inspect_opts), &blame_match/1)
|
||||
colorize(:red, banner) <> blame
|
||||
|
||||
|
|
|
@ -1,22 +1,161 @@
|
|||
defmodule Livebook.Runtime.Evaluator.Formatter do
|
||||
@moduledoc false
|
||||
|
||||
# Behaviour defining how evaluation results are transformed.
|
||||
#
|
||||
# The evaluation result is sent to the client as a message and it
|
||||
# may potentially be huge. If the client eventually converts the
|
||||
# result into some smaller representation, we would unnecessarily
|
||||
# send a lot of data. By defining a custom formatter the client can
|
||||
# instruct the `Evaluator` to send already transformed data.
|
||||
#
|
||||
# Additionally, if the results rely on external package installed
|
||||
# in the runtime node, then formatting anywhere else wouldn't be
|
||||
# accurate, for example using `inspect` on an external struct.
|
||||
|
||||
alias Livebook.Runtime.Evaluator
|
||||
require Logger
|
||||
|
||||
@doc """
|
||||
Transforms the evaluation result.
|
||||
Formats evaluation result into an output.
|
||||
|
||||
We format the result into one of the standardized runtime outputs.
|
||||
Other than that, formatting is important to convert potentially
|
||||
large result into a more compact representation. Finally, we want
|
||||
to format in the runtime node, because it oftentimes relies on the
|
||||
`inspect` protocol implementations from external packages.
|
||||
"""
|
||||
@callback format_result(Evaluator.evaluation_result()) :: term()
|
||||
@spec format_result(Livebook.Runtime.Evaluator.evaluation_result(), atom()) ::
|
||||
Livebook.Runtime.output()
|
||||
def format_result(result, language)
|
||||
|
||||
def format_result({:ok, :"do not show this result in output"}, :elixir) do
|
||||
# Functions in the `IEx.Helpers` module return this specific value
|
||||
# to indicate no result should be printed in the iex shell,
|
||||
# so we respect that as well.
|
||||
:ignored
|
||||
end
|
||||
|
||||
def format_result({:ok, {:module, _, _, _} = value}, :elixir) do
|
||||
to_inspect_output(value, limit: 10)
|
||||
end
|
||||
|
||||
def format_result({:ok, value}, :elixir) do
|
||||
to_output(value)
|
||||
end
|
||||
|
||||
def format_result({:error, kind, error, stacktrace}, _language) do
|
||||
formatted = format_error(kind, error, stacktrace)
|
||||
{:error, formatted, error_type(error)}
|
||||
end
|
||||
|
||||
def format_result({:ok, value}, :erlang) do
|
||||
erlang_to_output(value)
|
||||
end
|
||||
|
||||
@compile {:no_warn_undefined, {Kino.Render, :to_livebook, 1}}
|
||||
|
||||
defp to_output(value) do
|
||||
# Kino is a "client side" extension for Livebook that may be
|
||||
# installed into the runtime node. If it is installed we use
|
||||
# its more precise output rendering rules.
|
||||
if Code.ensure_loaded?(Kino.Render) do
|
||||
try do
|
||||
Kino.Render.to_livebook(value)
|
||||
catch
|
||||
kind, error ->
|
||||
formatted = format_error(kind, error, __STACKTRACE__)
|
||||
Logger.error(formatted)
|
||||
to_inspect_output(value)
|
||||
end
|
||||
else
|
||||
to_inspect_output(value)
|
||||
end
|
||||
end
|
||||
|
||||
defp to_inspect_output(value, opts \\ []) do
|
||||
try do
|
||||
inspected = inspect(value, inspect_opts(opts))
|
||||
{:text, inspected}
|
||||
catch
|
||||
kind, error ->
|
||||
formatted = format_error(kind, error, __STACKTRACE__)
|
||||
{:error, formatted, error_type(error)}
|
||||
end
|
||||
end
|
||||
|
||||
def inspect_opts(opts \\ []) do
|
||||
default_opts = [pretty: true, width: 100, syntax_colors: syntax_colors()]
|
||||
Keyword.merge(default_opts, opts)
|
||||
end
|
||||
|
||||
def syntax_colors() do
|
||||
# Note: we intentionally don't specify colors
|
||||
# for `:binary`, `:list`, `:map` and `:tuple`
|
||||
# and rely on these using the default text color.
|
||||
# This way we avoid a bunch of HTML tags for coloring commas, etc.
|
||||
[
|
||||
atom: :blue,
|
||||
# binary: :light_black,
|
||||
boolean: :magenta,
|
||||
# list: :light_black,
|
||||
# map: :light_black,
|
||||
number: :blue,
|
||||
nil: :magenta,
|
||||
regex: :red,
|
||||
string: :green,
|
||||
# tuple: :light_black,
|
||||
reset: :reset
|
||||
]
|
||||
end
|
||||
|
||||
defp format_error(kind, error, stacktrace) do
|
||||
{blamed, stacktrace} =
|
||||
case error do
|
||||
%CompileError{description: "cannot compile file (errors have been logged)" <> _, line: 0} ->
|
||||
{%CompileError{description: "cannot compile cell (errors have been logged)"}, []}
|
||||
|
||||
_ ->
|
||||
Exception.blame(kind, error, stacktrace)
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
defp error_type(%System.EnvError{env: "LB_" <> secret_name}),
|
||||
do: {:missing_secret, secret_name}
|
||||
|
||||
defp error_type(error) when is_struct(error, Kino.InterruptError),
|
||||
do: {:interrupt, error.variant, error.message}
|
||||
|
||||
defp error_type(_), do: :other
|
||||
|
||||
defp erlang_to_output(value) do
|
||||
text = :io_lib.format("~p", [value]) |> IO.iodata_to_binary()
|
||||
{:text, text}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
defmodule Livebook.Runtime.Evaluator.IdentityFormatter do
|
||||
@moduledoc false
|
||||
|
||||
# The default formatter leaving the output unchanged.
|
||||
|
||||
@behaviour Livebook.Runtime.Evaluator.Formatter
|
||||
|
||||
@impl true
|
||||
def format_result(evaluation_response), do: evaluation_response
|
||||
end
|
|
@ -2039,7 +2039,15 @@ defmodule Livebook.Session do
|
|||
|
||||
locator = {container_ref_for_section(section), cell.id}
|
||||
parent_locators = parent_locators_for_cell(state.data, cell)
|
||||
Runtime.evaluate_code(state.data.runtime, cell.source, locator, parent_locators, opts)
|
||||
|
||||
Runtime.evaluate_code(
|
||||
state.data.runtime,
|
||||
cell.language,
|
||||
cell.source,
|
||||
locator,
|
||||
parent_locators,
|
||||
opts
|
||||
)
|
||||
|
||||
state
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
defmodule LivebookWeb.SessionHelpers do
|
||||
import Phoenix.LiveView
|
||||
|
||||
use LivebookWeb, :verified_routes
|
||||
use LivebookWeb, :html
|
||||
|
||||
alias Phoenix.LiveView.Socket
|
||||
alias Livebook.Session
|
||||
|
@ -144,4 +144,58 @@ defmodule LivebookWeb.SessionHelpers do
|
|||
{:ok, Livebook.LiveMarkdown.notebook_from_livemd(content)}
|
||||
end
|
||||
end
|
||||
|
||||
def cell_icon(%{cell_type: :code, language: :elixir} = assigns) do
|
||||
~H"""
|
||||
<div class="flex w-6 h-6 bg-purple-100 rounded items-center justify-center">
|
||||
<svg width="11" height="15" viewBox="0 0 11 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M5.7784 3.58083C7.4569 5.87527 9.67878 5.70652 10.0618 9.04833C10.1147 12.9425 8.03684
|
||||
14.27 6.55353 14.6441C4.02227 15.3635 1.7644 14.2813 0.875648 11.8316C-0.83154 7.89408 2.36684
|
||||
1.41746 4.42502 0.0668945C4.60193 1.32119 5.05745 2.51995 5.75815 3.57521L5.7784 3.58083Z"
|
||||
fill="#663299"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def cell_icon(%{cell_type: :code, language: :erlang} = assigns) do
|
||||
~H"""
|
||||
<div class="flex w-6 h-6 bg-red-100 rounded items-center justify-center">
|
||||
<svg width="18" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 10">
|
||||
<g fill="#a90533">
|
||||
<path d="M2.4 10A7.7 7.7 0 0 1 .5 4.8c0-2 .6-3.6 1.6-4.8H0v10ZM13 10c.5-.6 1-1.2 1.4-2l-2.3-1.2c-.8 1.4-2 2.6-3.6 2.6-2.3 0-3.2-2-3.2-4.8H14V4c0-1.6-.3-3-1-4H15v10h-2Zm0 0" />
|
||||
<path d="M5.5 2.3c.1-1.2 1-2 2.1-2s1.9.8 2 2Zm0 0" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def cell_icon(%{cell_type: :markdown} = assigns) do
|
||||
~H"""
|
||||
<div class="flex w-6 h-6 bg-blue-100 rounded items-center justify-center">
|
||||
<svg width="16" height="14" viewBox="0 0 16 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.25 0.25H14.75C14.9489 0.25 15.1397 0.329018 15.2803 0.46967C15.421 0.610322 15.5 0.801088
|
||||
15.5 1V13C15.5 13.1989 15.421 13.3897 15.2803 13.5303C15.1397 13.671 14.9489 13.75 14.75 13.75H1.25C1.05109
|
||||
13.75 0.860322 13.671 0.71967 13.5303C0.579018 13.3897 0.5 13.1989 0.5 13V1C0.5 0.801088 0.579018 0.610322
|
||||
0.71967 0.46967C0.860322 0.329018 1.05109 0.25 1.25 0.25ZM4.25 9.625V6.625L5.75 8.125L7.25
|
||||
6.625V9.625H8.75V4.375H7.25L5.75 5.875L4.25 4.375H2.75V9.625H4.25ZM12.5 7.375V4.375H11V7.375H9.5L11.75
|
||||
9.625L14 7.375H12.5Z"
|
||||
fill="#3E64FF"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def cell_icon(%{cell_type: :smart} = assigns) do
|
||||
~H"""
|
||||
<div class="flex w-6 h-6 bg-red-100 rounded items-center justify-center">
|
||||
<.remix_icon icon="flashlight-line text-red-900" />
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
|
@ -391,6 +391,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
installing?={@data_view.installing?}
|
||||
allowed_uri_schemes={@allowed_uri_schemes}
|
||||
section_view={section_view}
|
||||
default_language={@data_view.default_language}
|
||||
/>
|
||||
<div style="height: 80vh"></div>
|
||||
</div>
|
||||
|
@ -935,7 +936,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
end
|
||||
|
||||
def handle_event("insert_cell_below", params, socket) do
|
||||
{type, attrs} = cell_type_and_attrs_from_params(params)
|
||||
{type, attrs} = cell_type_and_attrs_from_params(params, socket)
|
||||
|
||||
with {:ok, section, index} <-
|
||||
section_with_next_index(
|
||||
|
@ -1032,6 +1033,13 @@ defmodule LivebookWeb.SessionLive do
|
|||
end
|
||||
end
|
||||
|
||||
def handle_event("set_default_language", %{"language" => language}, socket)
|
||||
when language in ["elixir", "erlang"] do
|
||||
language = String.to_atom(language)
|
||||
Session.set_notebook_attributes(socket.assigns.session.pid, %{default_language: language})
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("delete_cell", %{"cell_id" => cell_id}, socket) do
|
||||
on_confirm = fn socket ->
|
||||
Session.delete_cell(socket.assigns.session.pid, cell_id)
|
||||
|
@ -1883,10 +1891,12 @@ defmodule LivebookWeb.SessionLive do
|
|||
String.upcase(head) <> tail
|
||||
end
|
||||
|
||||
defp cell_type_and_attrs_from_params(%{"type" => "markdown"}), do: {:markdown, %{}}
|
||||
defp cell_type_and_attrs_from_params(%{"type" => "code"}), do: {:code, %{}}
|
||||
defp cell_type_and_attrs_from_params(%{"type" => "markdown"}, _socket), do: {:markdown, %{}}
|
||||
|
||||
defp cell_type_and_attrs_from_params(%{"type" => "diagram"}) do
|
||||
defp cell_type_and_attrs_from_params(%{"type" => "code"}, socket),
|
||||
do: {:code, %{language: socket.private.data.notebook.default_language}}
|
||||
|
||||
defp cell_type_and_attrs_from_params(%{"type" => "diagram"}, _socket) do
|
||||
source = """
|
||||
<!-- Learn more at https://mermaid-js.github.io/mermaid -->
|
||||
|
||||
|
@ -1902,7 +1912,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
{:markdown, %{source: source}}
|
||||
end
|
||||
|
||||
defp cell_type_and_attrs_from_params(%{"type" => "image", "url" => url}) do
|
||||
defp cell_type_and_attrs_from_params(%{"type" => "image", "url" => url}, _socket) do
|
||||
source = ""
|
||||
|
||||
{:markdown, %{source: source}}
|
||||
|
@ -2129,6 +2139,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
file: data.file,
|
||||
persist_outputs: data.notebook.persist_outputs,
|
||||
autosave_interval_s: data.notebook.autosave_interval_s,
|
||||
default_language: data.notebook.default_language,
|
||||
dirty: data.dirty,
|
||||
runtime: data.runtime,
|
||||
smart_cell_definitions: data.smart_cell_definitions,
|
||||
|
@ -2241,6 +2252,7 @@ defmodule LivebookWeb.SessionLive do
|
|||
%{
|
||||
id: cell.id,
|
||||
type: :code,
|
||||
language: cell.language,
|
||||
empty: cell.source == "",
|
||||
eval: eval_info_to_view(cell, info.eval, data),
|
||||
reevaluate_automatically: cell.reevaluate_automatically
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
defmodule LivebookWeb.SessionLive.BinComponent do
|
||||
use LivebookWeb, :live_component
|
||||
|
||||
import LivebookWeb.SessionHelpers
|
||||
|
||||
alias Livebook.Notebook.Cell
|
||||
|
||||
@initial_limit 10
|
||||
|
@ -89,8 +91,8 @@ defmodule LivebookWeb.SessionLive.BinComponent do
|
|||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex py-1">
|
||||
<.cell_icon cell_type={Cell.type(cell)} />
|
||||
<p class="text-sm text-gray-700 self-center">
|
||||
<.cell_icon cell_type={Cell.type(cell)} language={Map.get(cell, :language)} />
|
||||
<p class="ml-1 text-sm text-gray-700 self-center">
|
||||
<span class="font-semibold">
|
||||
<%= Cell.type(cell) |> Atom.to_string() |> String.capitalize() %>
|
||||
</span>
|
||||
|
@ -143,49 +145,8 @@ defmodule LivebookWeb.SessionLive.BinComponent do
|
|||
"""
|
||||
end
|
||||
|
||||
defp cell_icon(%{cell_type: :code} = assigns) do
|
||||
~H"""
|
||||
<div class="flex w-6 h-6 bg-purple-100 rounded items-center justify-center mr-1">
|
||||
<svg width="11" height="15" viewBox="0 0 11 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M5.7784 3.58083C7.4569 5.87527 9.67878 5.70652 10.0618 9.04833C10.1147 12.9425 8.03684
|
||||
14.27 6.55353 14.6441C4.02227 15.3635 1.7644 14.2813 0.875648 11.8316C-0.83154 7.89408 2.36684
|
||||
1.41746 4.42502 0.0668945C4.60193 1.32119 5.05745 2.51995 5.75815 3.57521L5.7784 3.58083Z"
|
||||
fill="#663299"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp cell_icon(%{cell_type: :markdown} = assigns) do
|
||||
~H"""
|
||||
<div class="flex w-6 h-6 bg-blue-100 rounded items-center justify-center mr-1">
|
||||
<svg width="16" height="14" viewBox="0 0 16 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.25 0.25H14.75C14.9489 0.25 15.1397 0.329018 15.2803 0.46967C15.421 0.610322 15.5 0.801088
|
||||
15.5 1V13C15.5 13.1989 15.421 13.3897 15.2803 13.5303C15.1397 13.671 14.9489 13.75 14.75 13.75H1.25C1.05109
|
||||
13.75 0.860322 13.671 0.71967 13.5303C0.579018 13.3897 0.5 13.1989 0.5 13V1C0.5 0.801088 0.579018 0.610322
|
||||
0.71967 0.46967C0.860322 0.329018 1.05109 0.25 1.25 0.25ZM4.25 9.625V6.625L5.75 8.125L7.25
|
||||
6.625V9.625H8.75V4.375H7.25L5.75 5.875L4.25 4.375H2.75V9.625H4.25ZM12.5 7.375V4.375H11V7.375H9.5L11.75
|
||||
9.625L14 7.375H12.5Z"
|
||||
fill="#3E64FF"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp cell_icon(%{cell_type: :smart} = assigns) do
|
||||
~H"""
|
||||
<div class="flex w-6 h-6 bg-red-100 rounded items-center justify-center mr-1">
|
||||
<.remix_icon icon="flashlight-line text-red-900" />
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
defp cell_language(%Cell.Markdown{}), do: "markdown"
|
||||
defp cell_language(%Cell.Code{}), do: "elixir"
|
||||
defp cell_language(%Cell.Code{language: language}), do: Atom.to_string(language)
|
||||
defp cell_language(%Cell.Smart{}), do: "elixir"
|
||||
|
||||
@impl true
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
defmodule LivebookWeb.SessionLive.CellComponent do
|
||||
use LivebookWeb, :live_component
|
||||
|
||||
import LivebookWeb.SessionHelpers
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
|
@ -71,6 +73,9 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
/>
|
||||
</:primary>
|
||||
<:secondary>
|
||||
<div :if={@cell_view.language == :erlang} class="grayscale">
|
||||
<.cell_icon cell_type={:code} language={:erlang} />
|
||||
</div>
|
||||
<.amplify_output_button />
|
||||
<.cell_settings_button cell_id={@cell_view.id} session_id={@session_id} />
|
||||
<.cell_link_button cell_id={@cell_view.id} />
|
||||
|
@ -85,7 +90,7 @@ defmodule LivebookWeb.SessionLive.CellComponent do
|
|||
cell_id={@cell_view.id}
|
||||
tag="primary"
|
||||
empty={@cell_view.empty}
|
||||
language="elixir"
|
||||
language={@cell_view.language}
|
||||
intellisense
|
||||
/>
|
||||
<div class="absolute bottom-2 right-2">
|
||||
|
|
|
@ -25,7 +25,7 @@ defmodule LivebookWeb.SessionLive.CodeCellSettingsComponent do
|
|||
Cell settings
|
||||
</h3>
|
||||
<form phx-submit="save" phx-target={@myself}>
|
||||
<div class="w-full flex-col space-y-6">
|
||||
<div :if={@cell.language == :elixir} class="w-full flex-col space-y-6">
|
||||
<.switch_field
|
||||
name="enable_formatting"
|
||||
label="Format code when saving to file"
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
defmodule LivebookWeb.SessionLive.InsertButtonsComponent do
|
||||
use LivebookWeb, :live_component
|
||||
|
||||
import LivebookWeb.SessionHelpers
|
||||
|
||||
defguardp is_many(list) when tl(list) != []
|
||||
|
||||
def render(assigns) do
|
||||
|
@ -14,15 +16,41 @@ defmodule LivebookWeb.SessionLive.InsertButtonsComponent do
|
|||
<div class={
|
||||
"w-full absolute z-10 hover:z-[11] #{if(@persistent, do: "opacity-100", else: "opacity-0")} hover:opacity-100 focus-within:opacity-100 flex space-x-2 justify-center items-center"
|
||||
}>
|
||||
<button
|
||||
class="button-base button-small"
|
||||
phx-click="insert_cell_below"
|
||||
phx-value-type="code"
|
||||
phx-value-section_id={@section_id}
|
||||
phx-value-cell_id={@cell_id}
|
||||
>
|
||||
+ Code
|
||||
</button>
|
||||
<div class="relative">
|
||||
<div class="absolute -left-1 top-0 bottom-0 flex items-center transform -translate-x-full">
|
||||
<.menu id={"cell-#{@cell_id}-insert"} position={:bottom_left} distant>
|
||||
<:toggle>
|
||||
<button class="button-base button-small flex items-center pr-1">
|
||||
<div
|
||||
class="pr-2"
|
||||
phx-click="insert_cell_below"
|
||||
phx-value-type="code"
|
||||
phx-value-section_id={@section_id}
|
||||
phx-value-cell_id={@cell_id}
|
||||
phx-value-language="elixir"
|
||||
>
|
||||
+ <%= @default_language |> Atom.to_string() |> String.capitalize() %>
|
||||
</div>
|
||||
<div class="pl-1 flex items-center border-l border-gray-200">
|
||||
<.remix_icon icon="arrow-down-s-line" class="text-lg leading-none" />
|
||||
</div>
|
||||
</button>
|
||||
</:toggle>
|
||||
<.menu_item>
|
||||
<button role="menuitem" phx-click="set_default_language" phx-value-language="elixir">
|
||||
<.cell_icon cell_type={:code} language={:elixir} />
|
||||
<span>Elixir</span>
|
||||
</button>
|
||||
</.menu_item>
|
||||
<.menu_item>
|
||||
<button role="menuitem" phx-click="set_default_language" phx-value-language="erlang">
|
||||
<.cell_icon cell_type={:code} language={:erlang} />
|
||||
<span>Erlang</span>
|
||||
</button>
|
||||
</.menu_item>
|
||||
</.menu>
|
||||
</div>
|
||||
</div>
|
||||
<.menu id={"#{@id}-block-menu"} position={:bottom_left}>
|
||||
<:toggle>
|
||||
<button class="button-base button-small">+ Block</button>
|
||||
|
|
|
@ -170,6 +170,7 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
|
|||
section_id={@section_view.id}
|
||||
cell_id={nil}
|
||||
session_id={@session_id}
|
||||
default_language={@default_language}
|
||||
/>
|
||||
<%= for {cell_view, index} <- Enum.with_index(@section_view.cell_views) do %>
|
||||
<.live_component
|
||||
|
@ -193,6 +194,7 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
|
|||
section_id={@section_view.id}
|
||||
cell_id={cell_view.id}
|
||||
session_id={@session_id}
|
||||
default_language={@default_language}
|
||||
/>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
@ -83,6 +83,13 @@ defmodule Livebook.LiveMarkdown.ExportTest do
|
|||
""",
|
||||
chunks: [{0, 5}, {7, 5}],
|
||||
kind: "multi_chunk"
|
||||
},
|
||||
%{
|
||||
Notebook.Cell.new(:code)
|
||||
| language: :erlang,
|
||||
source: """
|
||||
lists:seq(1, 10).\
|
||||
"""
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -137,6 +144,10 @@ defmodule Livebook.LiveMarkdown.ExportTest do
|
|||
|
||||
x * x
|
||||
```
|
||||
|
||||
```erlang
|
||||
lists:seq(1, 10).
|
||||
```
|
||||
"""
|
||||
|
||||
document = Export.notebook_to_livemd(notebook)
|
||||
|
@ -1121,6 +1132,24 @@ defmodule Livebook.LiveMarkdown.ExportTest do
|
|||
assert expected_document == document
|
||||
end
|
||||
|
||||
test "persists :default_language when other than default" do
|
||||
notebook = %{
|
||||
Notebook.new()
|
||||
| name: "My Notebook",
|
||||
default_language: :erlang
|
||||
}
|
||||
|
||||
expected_document = """
|
||||
<!-- livebook:{"default_language":"erlang"} -->
|
||||
|
||||
# My Notebook
|
||||
"""
|
||||
|
||||
document = Export.notebook_to_livemd(notebook)
|
||||
|
||||
assert expected_document == document
|
||||
end
|
||||
|
||||
test "persists hub id when not default" do
|
||||
Livebook.Factory.build(:team, id: "team-persisted-id")
|
||||
|
||||
|
|
|
@ -54,6 +54,10 @@ defmodule Livebook.LiveMarkdown.ImportTest do
|
|||
|
||||
x * x
|
||||
```
|
||||
|
||||
```erlang
|
||||
lists:seq(1, 10).
|
||||
```
|
||||
"""
|
||||
|
||||
{notebook, []} = Import.notebook_from_livemd(markdown)
|
||||
|
@ -128,6 +132,12 @@ defmodule Livebook.LiveMarkdown.ImportTest do
|
|||
attrs: %{},
|
||||
chunks: [{0, 5}, {7, 5}],
|
||||
kind: "multi_chunk"
|
||||
},
|
||||
%Cell.Code{
|
||||
language: :erlang,
|
||||
source: """
|
||||
lists:seq(1, 10).\
|
||||
"""
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -720,6 +730,18 @@ defmodule Livebook.LiveMarkdown.ImportTest do
|
|||
assert %Notebook{name: "My Notebook", autosave_interval_s: 10} = notebook
|
||||
end
|
||||
|
||||
test "imports notebook :default_language attribute" do
|
||||
markdown = """
|
||||
<!-- livebook:{"default_language":"erlang"} -->
|
||||
|
||||
# My Notebook
|
||||
"""
|
||||
|
||||
{notebook, []} = Import.notebook_from_livemd(markdown)
|
||||
|
||||
assert %Notebook{name: "My Notebook", default_language: :erlang} = notebook
|
||||
end
|
||||
|
||||
test "imports notebook hub id when exists" do
|
||||
Livebook.Factory.insert_hub(:team, id: "team-persisted-id")
|
||||
|
||||
|
|
|
@ -133,4 +133,49 @@ defmodule Livebook.Notebook.Export.ElixirTest do
|
|||
assert expected_document == document
|
||||
end
|
||||
end
|
||||
|
||||
test "comments out non-elixir code cells" do
|
||||
notebook =
|
||||
%{
|
||||
Notebook.new()
|
||||
| name: "My Notebook",
|
||||
sections: [
|
||||
%{
|
||||
Notebook.Section.new()
|
||||
| name: "Section 1",
|
||||
cells: [
|
||||
%{
|
||||
Notebook.Cell.new(:code)
|
||||
| source: """
|
||||
Enum.to_list(1..10)\
|
||||
"""
|
||||
},
|
||||
%{
|
||||
Notebook.Cell.new(:code)
|
||||
| language: :erlang,
|
||||
source: """
|
||||
lists:seq(1, 10).\
|
||||
"""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
expected_document = """
|
||||
# Run as: iex --dot-iex path/to/notebook.exs
|
||||
|
||||
# Title: My Notebook
|
||||
|
||||
# ── Section 1 ──
|
||||
|
||||
Enum.to_list(1..10)
|
||||
|
||||
# lists:seq(1, 10).
|
||||
"""
|
||||
|
||||
document = Export.Elixir.notebook_to_elixir(notebook)
|
||||
|
||||
assert expected_document == document
|
||||
end
|
||||
end
|
||||
|
|
|
@ -35,7 +35,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
|
|||
|
||||
describe "evaluate_code/5" do
|
||||
test "spawns a new evaluator when necessary", %{pid: pid} do
|
||||
RuntimeServer.evaluate_code(pid, "1 + 1", {:c1, :e1}, [])
|
||||
RuntimeServer.evaluate_code(pid, :elixir, "1 + 1", {:c1, :e1}, [])
|
||||
|
||||
assert_receive {:runtime_evaluation_response, :e1, _, %{evaluation_time_ms: _time_ms}}
|
||||
end
|
||||
|
@ -44,8 +44,8 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
|
|||
stderr =
|
||||
ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
||||
code = "defmodule Foo do end"
|
||||
RuntimeServer.evaluate_code(pid, code, {:c1, :e1}, [])
|
||||
RuntimeServer.evaluate_code(pid, code, {:c1, :e2}, [])
|
||||
RuntimeServer.evaluate_code(pid, :elixir, code, {:c1, :e1}, [])
|
||||
RuntimeServer.evaluate_code(pid, :elixir, code, {:c1, :e2}, [])
|
||||
|
||||
assert_receive {:runtime_evaluation_response, :e1, _, %{evaluation_time_ms: _time_ms}}
|
||||
assert_receive {:runtime_evaluation_response, :e2, _, %{evaluation_time_ms: _time_ms}}
|
||||
|
@ -55,7 +55,13 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
|
|||
end
|
||||
|
||||
test "proxies evaluation stderr to evaluation stdout", %{pid: pid} do
|
||||
RuntimeServer.evaluate_code(pid, ~s{IO.puts(:stderr, "error to stdout")}, {:c1, :e1}, [])
|
||||
RuntimeServer.evaluate_code(
|
||||
pid,
|
||||
:elixir,
|
||||
~s{IO.puts(:stderr, "error to stdout")},
|
||||
{:c1, :e1},
|
||||
[]
|
||||
)
|
||||
|
||||
assert_receive {:runtime_evaluation_output, :e1, {:stdout, output}}
|
||||
|
||||
|
@ -69,17 +75,17 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
|
|||
Logger.error("hey")
|
||||
"""
|
||||
|
||||
RuntimeServer.evaluate_code(pid, code, {:c1, :e1}, [])
|
||||
RuntimeServer.evaluate_code(pid, :elixir, code, {:c1, :e1}, [])
|
||||
|
||||
assert_receive {:runtime_evaluation_output, :e1, {:stdout, log_message}}
|
||||
assert log_message =~ "[error] hey"
|
||||
end
|
||||
|
||||
test "supports cross-container evaluation context references", %{pid: pid} do
|
||||
RuntimeServer.evaluate_code(pid, "x = 1", {:c1, :e1}, [])
|
||||
RuntimeServer.evaluate_code(pid, :elixir, "x = 1", {:c1, :e1}, [])
|
||||
assert_receive {:runtime_evaluation_response, :e1, _, %{evaluation_time_ms: _time_ms}}
|
||||
|
||||
RuntimeServer.evaluate_code(pid, "x", {:c2, :e2}, [{:c1, :e1}])
|
||||
RuntimeServer.evaluate_code(pid, :elixir, "x", {:c2, :e2}, [{:c1, :e1}])
|
||||
|
||||
assert_receive {:runtime_evaluation_response, :e2, {:text, "\e[34m1\e[0m"},
|
||||
%{evaluation_time_ms: _time_ms}}
|
||||
|
@ -108,7 +114,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
|
|||
pid = spawn(fn -> loop.(loop, %{callers: [], count: 0}) end)
|
||||
"""
|
||||
|
||||
RuntimeServer.evaluate_code(pid, code, {:c1, :e1}, [])
|
||||
RuntimeServer.evaluate_code(pid, :elixir, code, {:c1, :e1}, [])
|
||||
assert_receive {:runtime_evaluation_response, :e1, _, %{evaluation_time_ms: _time_ms}}
|
||||
|
||||
await_code = """
|
||||
|
@ -122,8 +128,8 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
|
|||
# Note: it's important to first start evaluation in :c2,
|
||||
# because it needs to copy evaluation context from :c1
|
||||
|
||||
RuntimeServer.evaluate_code(pid, await_code, {:c2, :e2}, [{:c1, :e1}])
|
||||
RuntimeServer.evaluate_code(pid, await_code, {:c1, :e3}, [{:c1, :e1}])
|
||||
RuntimeServer.evaluate_code(pid, :elixir, await_code, {:c2, :e2}, [{:c1, :e1}])
|
||||
RuntimeServer.evaluate_code(pid, :elixir, await_code, {:c1, :e3}, [{:c1, :e1}])
|
||||
|
||||
assert_receive {:runtime_evaluation_response, :e2, _, %{evaluation_time_ms: _time_ms}}
|
||||
assert_receive {:runtime_evaluation_response, :e3, _, %{evaluation_time_ms: _time_ms}}
|
||||
|
@ -145,7 +151,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
|
|||
number = 10
|
||||
"""
|
||||
|
||||
RuntimeServer.evaluate_code(pid, code, {:c1, :e1}, [])
|
||||
RuntimeServer.evaluate_code(pid, :elixir, code, {:c1, :e1}, [])
|
||||
assert_receive {:runtime_evaluation_response, :e1, _, %{evaluation_time_ms: _time_ms}}
|
||||
|
||||
request = {:completion, "num"}
|
||||
|
@ -198,7 +204,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
|
|||
Task.async(fn -> raise "error" end)
|
||||
"""
|
||||
|
||||
RuntimeServer.evaluate_code(pid, code, {:c1, :e1}, [])
|
||||
RuntimeServer.evaluate_code(pid, :elixir, code, {:c1, :e1}, [])
|
||||
|
||||
assert_receive {:runtime_container_down, :c1, message}
|
||||
assert message =~ "(RuntimeError) error"
|
||||
|
@ -265,7 +271,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
|
|||
|
||||
@tag opts: @opts
|
||||
test "scans binding when a new parent locators are set", %{pid: pid} do
|
||||
RuntimeServer.evaluate_code(pid, "1 + 1", {:c1, :e1}, [])
|
||||
RuntimeServer.evaluate_code(pid, :elixir, "1 + 1", {:c1, :e1}, [])
|
||||
RuntimeServer.start_smart_cell(pid, "dumb", "ref", %{}, [])
|
||||
assert_receive {:smart_cell_debug, "ref", :handle_info, :scan_binding_ping}
|
||||
RuntimeServer.set_smart_cell_parent_locators(pid, "ref", [{:c1, :e1}])
|
||||
|
@ -274,17 +280,17 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
|
|||
|
||||
@tag opts: @opts
|
||||
test "scans binding when one of the parent locators is evaluated", %{pid: pid} do
|
||||
RuntimeServer.evaluate_code(pid, "1 + 1", {:c1, :e1}, [])
|
||||
RuntimeServer.evaluate_code(pid, :elixir, "1 + 1", {:c1, :e1}, [])
|
||||
RuntimeServer.start_smart_cell(pid, "dumb", "ref", %{}, [{:c1, :e1}])
|
||||
assert_receive {:smart_cell_debug, "ref", :handle_info, :scan_binding_ping}
|
||||
RuntimeServer.evaluate_code(pid, "1 + 1", {:c1, :e1}, [])
|
||||
RuntimeServer.evaluate_code(pid, :elixir, "1 + 1", {:c1, :e1}, [])
|
||||
assert_receive {:smart_cell_debug, "ref", :handle_info, :scan_binding_ping}
|
||||
end
|
||||
|
||||
@tag opts: @opts
|
||||
test "scans evaluation result when the smart cell is evaluated", %{pid: pid} do
|
||||
RuntimeServer.start_smart_cell(pid, "dumb", "ref", %{}, [])
|
||||
RuntimeServer.evaluate_code(pid, "1 + 1", {:c1, :e1}, [], smart_cell_ref: "ref")
|
||||
RuntimeServer.evaluate_code(pid, :elixir, "1 + 1", {:c1, :e1}, [], smart_cell_ref: "ref")
|
||||
assert_receive {:smart_cell_debug, "ref", :handle_info, :scan_eval_result_ping}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
defmodule Livebook.Runtime.Evaluator.DefaultFormatterTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Livebook.Runtime.Evaluator.DefaultFormatter
|
||||
|
||||
test "inspects successful results" do
|
||||
result = 10
|
||||
assert {:text, "\e[34m10\e[0m"} = DefaultFormatter.format_result({:ok, result})
|
||||
end
|
||||
|
||||
test "gracefully handles errors in the inspect protocol" do
|
||||
result = %Livebook.TestModules.BadInspect{}
|
||||
assert {:error, error} = DefaultFormatter.format_result({:ok, result})
|
||||
assert error =~ ":bad_return"
|
||||
end
|
||||
end
|
|
@ -36,6 +36,9 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
end
|
||||
end
|
||||
|
||||
defmacrop ansi_number(number), do: "\e[34m#{number}\e[0m"
|
||||
defmacrop ansi_string(string), do: "\e[32m\"#{string}\"\e[0m"
|
||||
|
||||
describe "evaluate_code/6" do
|
||||
test "given a valid code returns evaluation result", %{evaluator: evaluator} do
|
||||
code = """
|
||||
|
@ -44,9 +47,11 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
x + y
|
||||
"""
|
||||
|
||||
Evaluator.evaluate_code(evaluator, code, :code_1, [])
|
||||
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, {:ok, 3}, metadata() = metadata}
|
||||
assert metadata.evaluation_time_ms >= 0
|
||||
|
||||
assert %{atom: _, binary: _, code: _, ets: _, other: _, processes: _, total: _} =
|
||||
|
@ -54,67 +59,69 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
end
|
||||
|
||||
test "given no parent refs does not see previous evaluation context", %{evaluator: evaluator} do
|
||||
Evaluator.evaluate_code(evaluator, "x = 1", :code_1, [])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, "x = 1", :code_1, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_1, _, metadata()}
|
||||
|
||||
Evaluator.evaluate_code(evaluator, "x", :code_2, [])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, "x", :code_2, [])
|
||||
|
||||
assert_receive {:runtime_evaluation_response, :code_2,
|
||||
{:error, _kind, %CompileError{}, _stacktrace}, metadata()}
|
||||
{:error,
|
||||
"\e[31m** (CompileError) cannot compile cell (errors have been logged)\e[0m",
|
||||
:other}, metadata()}
|
||||
end
|
||||
|
||||
test "given parent refs sees previous evaluation context", %{evaluator: evaluator} do
|
||||
Evaluator.evaluate_code(evaluator, "x = 1", :code_1, [])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, "x = 1", :code_1, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_1, _, metadata()}
|
||||
|
||||
Evaluator.evaluate_code(evaluator, "x", :code_2, [:code_1])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, "x", :code_2, [:code_1])
|
||||
|
||||
assert_receive {:runtime_evaluation_response, :code_2, {:ok, 1}, metadata()}
|
||||
assert_receive {:runtime_evaluation_response, :code_2, {:text, ansi_number(1)}, metadata()}
|
||||
end
|
||||
|
||||
test "given invalid parent ref uses the default context", %{evaluator: evaluator} do
|
||||
Evaluator.evaluate_code(evaluator, ":hey", :code_1, [:code_nonexistent])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, "1", :code_1, [:code_nonexistent])
|
||||
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:ok, :hey}, metadata()}
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:text, ansi_number(1)}, metadata()}
|
||||
end
|
||||
|
||||
test "given parent refs sees previous process dictionary", %{evaluator: evaluator} do
|
||||
Evaluator.evaluate_code(evaluator, "Process.put(:x, 1)", :code_1, [])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, "Process.put(:x, 1)", :code_1, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_1, _, metadata()}
|
||||
Evaluator.evaluate_code(evaluator, "Process.put(:x, 2)", :code_2, [])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, "Process.put(:x, 2)", :code_2, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_2, _, metadata()}
|
||||
|
||||
Evaluator.evaluate_code(evaluator, "Process.get(:x)", :code_3, [:code_1])
|
||||
assert_receive {:runtime_evaluation_response, :code_3, {:ok, 1}, 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()}
|
||||
|
||||
Evaluator.evaluate_code(evaluator, "Process.get(:x)", :code_3, [:code_2])
|
||||
assert_receive {:runtime_evaluation_response, :code_3, {:ok, 2}, 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()}
|
||||
end
|
||||
|
||||
test "keeps :rand state intact in process dictionary", %{evaluator: evaluator} do
|
||||
Evaluator.evaluate_code(evaluator, ":rand.seed(:default, 0)", :code_1, [])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, ":rand.seed(:default, 0)", :code_1, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_1, _, metadata()}
|
||||
|
||||
Evaluator.evaluate_code(evaluator, ":rand.uniform()", :code_2, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_2, {:ok, number1}, metadata()}
|
||||
Evaluator.evaluate_code(evaluator, :elixir, ":rand.uniform()", :code_2, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_2, {:text, result1}, metadata()}
|
||||
|
||||
Evaluator.evaluate_code(evaluator, ":rand.uniform()", :code_2, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_2, {:ok, number2}, metadata()}
|
||||
Evaluator.evaluate_code(evaluator, :elixir, ":rand.uniform()", :code_2, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_2, {:text, result2}, metadata()}
|
||||
|
||||
assert number1 != number2
|
||||
assert result1 != result2
|
||||
|
||||
Evaluator.evaluate_code(evaluator, ":rand.seed(:default, 0)", :code_1, [])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, ":rand.seed(:default, 0)", :code_1, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_1, _, metadata()}
|
||||
|
||||
Evaluator.evaluate_code(evaluator, ":rand.uniform()", :code_2, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_2, {:ok, ^number1}, metadata()}
|
||||
Evaluator.evaluate_code(evaluator, :elixir, ":rand.uniform()", :code_2, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_2, {:text, ^result1}, metadata()}
|
||||
|
||||
Evaluator.evaluate_code(evaluator, ":rand.uniform()", :code_2, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_2, {:ok, ^number2}, metadata()}
|
||||
Evaluator.evaluate_code(evaluator, :elixir, ":rand.uniform()", :code_2, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_2, {:text, ^result2}, metadata()}
|
||||
end
|
||||
|
||||
test "captures standard output and sends it to the caller", %{evaluator: evaluator} do
|
||||
Evaluator.evaluate_code(evaluator, ~s{IO.puts("hey")}, :code_1, [])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, ~s{IO.puts("hey")}, :code_1, [])
|
||||
|
||||
assert_receive {:runtime_evaluation_output, :code_1, {:stdout, "hey\n"}}
|
||||
end
|
||||
|
@ -129,12 +136,12 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
end
|
||||
"""
|
||||
|
||||
Evaluator.evaluate_code(evaluator, code, :code_1, [])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [])
|
||||
|
||||
assert_receive {:runtime_evaluation_input, :code_1, reply_to, "input1"}
|
||||
send(reply_to, {:runtime_evaluation_input_reply, {:ok, :value}})
|
||||
send(reply_to, {:runtime_evaluation_input_reply, {:ok, 10}})
|
||||
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:ok, :value}, metadata()}
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:text, ansi_number(10)}, metadata()}
|
||||
end
|
||||
|
||||
test "returns error along with its kind and stacktrace", %{evaluator: evaluator} do
|
||||
|
@ -142,23 +149,38 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
List.first(%{})
|
||||
"""
|
||||
|
||||
Evaluator.evaluate_code(evaluator, code, :code_1, [], file: "file.ex")
|
||||
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [], file: "file.ex")
|
||||
|
||||
assert_receive {:runtime_evaluation_response, :code_1,
|
||||
{:error, :error, :function_clause,
|
||||
[
|
||||
{List, :first, _arity, _location1},
|
||||
{:elixir_eval, :__FILE__, 1, _location2}
|
||||
]}, metadata()}
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:error, message, :other},
|
||||
metadata()}
|
||||
|
||||
assert clean_message(message) == """
|
||||
** (FunctionClauseError) no function clause matching in List.first/2
|
||||
|
||||
The following arguments were given to List.first/2:
|
||||
|
||||
# 1
|
||||
%{}
|
||||
|
||||
# 2
|
||||
nil
|
||||
|
||||
Attempted function clauses (showing 2 out of 2):
|
||||
|
||||
def first([], default)
|
||||
def first([head | _], _default)
|
||||
|
||||
(elixir 1.15.0-rc.1) lib/list.ex:293: List.first/2
|
||||
file.ex:1: (file)
|
||||
"""
|
||||
end
|
||||
|
||||
test "returns additional metadata when there is a syntax error", %{evaluator: evaluator} do
|
||||
code = "1+"
|
||||
|
||||
Evaluator.evaluate_code(evaluator, code, :code_1, [], file: "file.ex")
|
||||
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [], file: "file.ex")
|
||||
|
||||
assert_receive {:runtime_evaluation_response, :code_1,
|
||||
{:error, :error, %TokenMissingError{}, []},
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:error, message, :other},
|
||||
%{
|
||||
code_markers: [
|
||||
%{
|
||||
|
@ -168,15 +190,24 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
}
|
||||
]
|
||||
}}
|
||||
|
||||
assert clean_message(message) === """
|
||||
** (TokenMissingError) file.ex:1:2: syntax error: expression is incomplete
|
||||
|
|
||||
1 | 1+
|
||||
| ^\
|
||||
"""
|
||||
end
|
||||
|
||||
test "returns additional metadata when there is a compilation error", %{evaluator: evaluator} do
|
||||
code = "x"
|
||||
|
||||
Evaluator.evaluate_code(evaluator, code, :code_1, [], file: "file.ex")
|
||||
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [], file: "file.ex")
|
||||
|
||||
assert_receive {:runtime_evaluation_response, :code_1,
|
||||
{:error, :error, %CompileError{}, _stacktrace},
|
||||
{:error,
|
||||
"\e[31m** (CompileError) cannot compile cell (errors have been logged)\e[0m",
|
||||
:other},
|
||||
%{
|
||||
code_markers: [
|
||||
%{
|
||||
|
@ -193,16 +224,12 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
Code.eval_string("x")
|
||||
"""
|
||||
|
||||
Evaluator.evaluate_code(evaluator, code, :code_1, [], file: "file.ex")
|
||||
|
||||
expected_stacktrace = [
|
||||
{:elixir_expand, :expand, 3, [file: ~c"src/elixir_expand.erl", line: 383]},
|
||||
{:elixir_eval, :__FILE__, 1, [file: ~c"file.ex", line: 1]}
|
||||
]
|
||||
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [], file: "file.ex")
|
||||
|
||||
assert_receive {:runtime_evaluation_response, :code_1,
|
||||
{:error, :error, %CompileError{}, ^expected_stacktrace},
|
||||
%{code_markers: []}}
|
||||
{:error,
|
||||
"\e[31m** (CompileError) cannot compile cell (errors have been logged)\e[0m",
|
||||
:other}, %{code_markers: []}}
|
||||
end
|
||||
|
||||
test "in case of an error returns only the relevant part of stacktrace",
|
||||
|
@ -225,19 +252,20 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
Livebook.Runtime.EvaluatorTest.Stacktrace.Cat.meow()
|
||||
"""
|
||||
|
||||
Evaluator.evaluate_code(evaluator, code, :code_1, [])
|
||||
|
||||
expected_stacktrace = [
|
||||
{Livebook.Runtime.EvaluatorTest.Stacktrace.Math, :bad_math, 0,
|
||||
[file: ~c"nofile", line: 3]},
|
||||
{Livebook.Runtime.EvaluatorTest.Stacktrace.Cat, :meow, 0, [file: ~c"nofile", line: 10]},
|
||||
{:elixir_eval, :__FILE__, 1, [file: ~c"nofile", line: 15]}
|
||||
]
|
||||
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [])
|
||||
|
||||
# Note: evaluating module definitions is relatively slow, so we use a higher wait timeout.
|
||||
assert_receive {:runtime_evaluation_response, :code_1,
|
||||
{:error, _kind, _error, ^expected_stacktrace}, metadata()},
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:error, message, :other},
|
||||
metadata()},
|
||||
2_000
|
||||
|
||||
assert clean_message(message) ==
|
||||
"""
|
||||
** (ArithmeticError) bad argument in arithmetic expression
|
||||
nofile:3: Livebook.Runtime.EvaluatorTest.Stacktrace.Math.bad_math/0
|
||||
nofile:10: Livebook.Runtime.EvaluatorTest.Stacktrace.Cat.meow/0
|
||||
nofile:15: (file)
|
||||
"""
|
||||
end
|
||||
|
||||
test "in case of an error uses empty evaluation context as the resulting context",
|
||||
|
@ -254,15 +282,15 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
x * x
|
||||
"""
|
||||
|
||||
Evaluator.evaluate_code(evaluator, code1, :code_1, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:ok, _}, metadata()}
|
||||
Evaluator.evaluate_code(evaluator, :elixir, code1, :code_1, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:text, ansi_number(2)}, metadata()}
|
||||
|
||||
Evaluator.evaluate_code(evaluator, code2, :code_2, [:code_1])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, code2, :code_2, [:code_1])
|
||||
|
||||
assert_receive {:runtime_evaluation_response, :code_2, {:error, _, _, _}, metadata()}
|
||||
assert_receive {:runtime_evaluation_response, :code_2, {:error, _, _}, metadata()}
|
||||
|
||||
Evaluator.evaluate_code(evaluator, code3, :code_3, [:code_2, :code_1])
|
||||
assert_receive {:runtime_evaluation_response, :code_3, {:ok, 4}, 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()}
|
||||
end
|
||||
|
||||
test "given file option sets it in evaluation environment", %{evaluator: evaluator} do
|
||||
|
@ -271,9 +299,10 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
"""
|
||||
|
||||
opts = [file: "/path/dir/file"]
|
||||
Evaluator.evaluate_code(evaluator, code, :code_1, [], opts)
|
||||
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [], opts)
|
||||
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:ok, "/path/dir"}, metadata()}
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:text, ansi_string("/path/dir")},
|
||||
metadata()}
|
||||
end
|
||||
|
||||
test "kills widgets that that no evaluation points to", %{evaluator: evaluator} do
|
||||
|
@ -281,15 +310,21 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
# The evaluation reference is the same, so the second one overrides
|
||||
# the first one and the first widget should eventually be killed.
|
||||
|
||||
Evaluator.evaluate_code(evaluator, spawn_widget_code(), :code_1, [])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, spawn_widget_code(), :code_1, [])
|
||||
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:ok, widget_pid1}, metadata()}
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:text, widget_pid1_string},
|
||||
metadata()}
|
||||
|
||||
widget_pid1 = IEx.Helpers.pid(widget_pid1_string)
|
||||
|
||||
ref = Process.monitor(widget_pid1)
|
||||
|
||||
Evaluator.evaluate_code(evaluator, spawn_widget_code(), :code_1, [])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, spawn_widget_code(), :code_1, [])
|
||||
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:ok, widget_pid2}, metadata()}
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:text, widget_pid2_string},
|
||||
metadata()}
|
||||
|
||||
widget_pid2 = IEx.Helpers.pid(widget_pid2_string)
|
||||
|
||||
assert_receive {:DOWN, ^ref, :process, ^widget_pid1, _reason}
|
||||
|
||||
|
@ -302,12 +337,16 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
|
||||
Evaluator.evaluate_code(
|
||||
evaluator,
|
||||
:elixir,
|
||||
spawn_widget_from_terminating_process_code(),
|
||||
:code_1,
|
||||
[]
|
||||
)
|
||||
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:ok, widget_pid1}, metadata()}
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:text, widget_pid1_string},
|
||||
metadata()}
|
||||
|
||||
widget_pid1 = IEx.Helpers.pid(widget_pid1_string)
|
||||
|
||||
ref = Process.monitor(widget_pid1)
|
||||
assert_receive {:DOWN, ^ref, :process, ^widget_pid1, _reason}
|
||||
|
@ -320,17 +359,19 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
end
|
||||
"""
|
||||
|
||||
Evaluator.evaluate_code(evaluator, code, :code_1, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:ok, _}, metadata()}
|
||||
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:text, _}, metadata()}
|
||||
|
||||
# Redefining in the same evaluation works
|
||||
Evaluator.evaluate_code(evaluator, code, :code_1, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:ok, _}, metadata()}
|
||||
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:text, _}, metadata()}
|
||||
|
||||
Evaluator.evaluate_code(evaluator, code, :code_2, [], file: "file.ex")
|
||||
Evaluator.evaluate_code(evaluator, :elixir, code, :code_2, [], file: "file.ex")
|
||||
|
||||
assert_receive {:runtime_evaluation_response, :code_2,
|
||||
{:error, :error, %CompileError{}, []},
|
||||
{:error,
|
||||
"\e[31m** (CompileError) file.ex:1: module Livebook.Runtime.EvaluatorTest.Redefinition is already defined\e[0m",
|
||||
:other},
|
||||
%{
|
||||
code_markers: [
|
||||
%{
|
||||
|
@ -352,8 +393,8 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
end
|
||||
"""
|
||||
|
||||
Evaluator.evaluate_code(evaluator, code, :code_1, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:ok, _}, metadata()}
|
||||
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:text, _}, metadata()}
|
||||
|
||||
assert File.exists?(Path.join(ebin_path, "Elixir.Livebook.Runtime.EvaluatorTest.Disk.beam"))
|
||||
|
||||
|
@ -368,8 +409,8 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
raise "failed"
|
||||
"""
|
||||
|
||||
Evaluator.evaluate_code(evaluator, code, :code_1, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:error, _, _, _}, metadata()}
|
||||
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:error, _, _}, metadata()}
|
||||
|
||||
refute Code.ensure_loaded?(Livebook.Runtime.EvaluatorTest.Raised)
|
||||
end
|
||||
|
@ -386,7 +427,7 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
|
||||
{:group_leader, gl} = Process.info(evaluator.pid, :group_leader)
|
||||
|
||||
Evaluator.evaluate_code(evaluator, code, :code_1, [])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [])
|
||||
|
||||
ref = Process.monitor(gl)
|
||||
assert_receive {:DOWN, ^ref, :process, ^gl, _reason}
|
||||
|
@ -431,7 +472,7 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
end
|
||||
'''
|
||||
|
||||
Evaluator.evaluate_code(evaluator, code, :code_1, [])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [])
|
||||
|
||||
assert_receive {:runtime_doctest_report, :code_1, %{line: 4, status: :running}}
|
||||
|
||||
|
@ -484,7 +525,7 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
end
|
||||
'''
|
||||
|
||||
Evaluator.evaluate_code(evaluator, code, :code_1, [])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [])
|
||||
|
||||
assert_receive {:runtime_doctest_report, :code_1, %{line: 4, status: :running}}
|
||||
|
||||
|
@ -525,7 +566,7 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
end
|
||||
'''
|
||||
|
||||
Evaluator.evaluate_code(evaluator, code, :code_1, [])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [])
|
||||
|
||||
assert_receive {:runtime_doctest_report, :code_1, %{line: 4, status: :running}}
|
||||
|
||||
|
@ -576,7 +617,7 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
end
|
||||
'''
|
||||
|
||||
Evaluator.evaluate_code(evaluator, code, :code_1, [])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [])
|
||||
|
||||
assert_receive {:runtime_doctest_report, :code_1, %{line: 4, status: :running}}
|
||||
|
||||
|
@ -595,8 +636,8 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
defp eval(code, evaluator, eval_idx) do
|
||||
ref = eval_idx
|
||||
parent_refs = Enum.to_list((eval_idx - 1)..0//-1)
|
||||
Evaluator.evaluate_code(evaluator, code, ref, parent_refs)
|
||||
assert_receive {:runtime_evaluation_response, ^ref, {:ok, _}, metadata}
|
||||
Evaluator.evaluate_code(evaluator, :elixir, code, ref, parent_refs)
|
||||
assert_receive {:runtime_evaluation_response, ^ref, {:text, _}, metadata}
|
||||
%{used: metadata.identifiers_used, defined: metadata.identifiers_defined}
|
||||
end
|
||||
|
||||
|
@ -996,21 +1037,26 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
|
||||
describe "forget_evaluation/2" do
|
||||
test "invalidates the given reference", %{evaluator: evaluator} do
|
||||
Evaluator.evaluate_code(evaluator, "x = 1", :code_1, [])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, "x = 1", :code_1, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_1, _, metadata()}
|
||||
|
||||
Evaluator.forget_evaluation(evaluator, :code_1)
|
||||
|
||||
Evaluator.evaluate_code(evaluator, "x", :code_2, [:code_1])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, "x", :code_2, [:code_1])
|
||||
|
||||
assert_receive {:runtime_evaluation_response, :code_2,
|
||||
{:error, _kind, %CompileError{}, _stacktrace}, metadata()}
|
||||
{:error,
|
||||
"\e[31m** (CompileError) cannot compile cell (errors have been logged)\e[0m",
|
||||
:other}, metadata()}
|
||||
end
|
||||
|
||||
test "kills widgets that no evaluation points to", %{evaluator: evaluator} do
|
||||
Evaluator.evaluate_code(evaluator, spawn_widget_code(), :code_1, [])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, spawn_widget_code(), :code_1, [])
|
||||
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:ok, widget_pid1}, metadata()}
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:text, widget_pid1_string},
|
||||
metadata()}
|
||||
|
||||
widget_pid1 = IEx.Helpers.pid(widget_pid1_string)
|
||||
|
||||
ref = Process.monitor(widget_pid1)
|
||||
Evaluator.forget_evaluation(evaluator, :code_1)
|
||||
|
@ -1025,14 +1071,14 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
end
|
||||
"""
|
||||
|
||||
Evaluator.evaluate_code(evaluator, code, :code_1, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:ok, _}, metadata()}
|
||||
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:text, _}, metadata()}
|
||||
|
||||
Evaluator.forget_evaluation(evaluator, :code_1)
|
||||
|
||||
# Define the module in a different evaluation
|
||||
Evaluator.evaluate_code(evaluator, code, :code_2, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_2, {:ok, _}, metadata()}
|
||||
Evaluator.evaluate_code(evaluator, :elixir, code, :code_2, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_2, {:text, _}, metadata()}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1048,22 +1094,22 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
|
||||
test "copies the given context and sets as the initial one",
|
||||
%{evaluator: evaluator, parent_evaluator: parent_evaluator} do
|
||||
Evaluator.evaluate_code(parent_evaluator, "x = 1", :code_1, [])
|
||||
Evaluator.evaluate_code(parent_evaluator, :elixir, "x = 1", :code_1, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_1, _, metadata()}
|
||||
|
||||
Evaluator.initialize_from(evaluator, parent_evaluator, [:code_1])
|
||||
|
||||
Evaluator.evaluate_code(evaluator, "x", :code_2, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_2, {:ok, 1}, metadata()}
|
||||
Evaluator.evaluate_code(evaluator, :elixir, "x", :code_2, [])
|
||||
assert_receive {:runtime_evaluation_response, :code_2, {:text, ansi_number(1)}, metadata()}
|
||||
end
|
||||
end
|
||||
|
||||
describe "binding order" do
|
||||
test "keeps binding in evaluation order, starting from most recent", %{evaluator: evaluator} do
|
||||
Evaluator.evaluate_code(evaluator, "b = 1", :code_1, [])
|
||||
Evaluator.evaluate_code(evaluator, "a = 1", :code_2, [:code_1])
|
||||
Evaluator.evaluate_code(evaluator, "c = 1", :code_3, [:code_2, :code_1])
|
||||
Evaluator.evaluate_code(evaluator, "x = 1", :code_4, [:code_3, :code_2, :code_1])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, "b = 1", :code_1, [])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, "a = 1", :code_2, [:code_1])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, "c = 1", :code_3, [:code_2, :code_1])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, "x = 1", :code_4, [:code_3, :code_2, :code_1])
|
||||
|
||||
%{binding: binding} =
|
||||
Evaluator.get_evaluation_context(evaluator, [:code_4, :code_3, :code_2, :code_1])
|
||||
|
@ -1072,9 +1118,9 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
end
|
||||
|
||||
test "treats rebound names as new", %{evaluator: evaluator} do
|
||||
Evaluator.evaluate_code(evaluator, "b = 1", :code_1, [])
|
||||
Evaluator.evaluate_code(evaluator, "a = 1", :code_2, [:code_1])
|
||||
Evaluator.evaluate_code(evaluator, "b = 2", :code_3, [:code_2, :code_1])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, "b = 1", :code_1, [])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, "a = 1", :code_2, [:code_1])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, "b = 2", :code_3, [:code_2, :code_1])
|
||||
|
||||
%{binding: binding} =
|
||||
Evaluator.get_evaluation_context(evaluator, [:code_3, :code_2, :code_1])
|
||||
|
@ -1083,6 +1129,59 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
end
|
||||
end
|
||||
|
||||
describe "erlang evaluation" do
|
||||
test "evaluate erlang code", %{evaluator: evaluator} do
|
||||
Evaluator.evaluate_code(
|
||||
evaluator,
|
||||
:erlang,
|
||||
"X = lists:seq(1, 3), lists:sum(X).",
|
||||
:code_1,
|
||||
[]
|
||||
)
|
||||
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:text, "6"}, metadata()}
|
||||
end
|
||||
|
||||
test "mixed erlang/elixir bindings", %{evaluator: evaluator} do
|
||||
Evaluator.evaluate_code(evaluator, :elixir, "x = 1", :code_1, [])
|
||||
Evaluator.evaluate_code(evaluator, :erlang, "Y = X.", :code_2, [:code_1])
|
||||
Evaluator.evaluate_code(evaluator, :elixir, "z = y", :code_3, [:code_2])
|
||||
|
||||
%{binding: binding} =
|
||||
Evaluator.get_evaluation_context(evaluator, [:code_3, :code_2, :code_1])
|
||||
|
||||
assert [{:z, 1}, {:y, 1}, {:x, 1}] == binding
|
||||
end
|
||||
|
||||
test "inspects erlang results using erlang format", %{evaluator: evaluator} 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()}
|
||||
end
|
||||
|
||||
test "does not return error marker on empty source", %{evaluator: evaluator} do
|
||||
Evaluator.evaluate_code(evaluator, :erlang, "", :code_1, [])
|
||||
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:error, _, _},
|
||||
metadata() = metadata}
|
||||
|
||||
assert metadata.code_markers == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "formatting" do
|
||||
test "gracefully handles errors in the inspect protocol", %{evaluator: evaluator} do
|
||||
code = "%Livebook.TestModules.BadInspect{}"
|
||||
Evaluator.evaluate_code(evaluator, :elixir, code, :code_1, [], file: "file.ex")
|
||||
|
||||
assert_receive {:runtime_evaluation_response, :code_1, {:error, message, :other},
|
||||
metadata()}
|
||||
|
||||
assert message =~ ":bad_return"
|
||||
end
|
||||
end
|
||||
|
||||
# Helpers
|
||||
|
||||
# Returns a code that spawns a widget process, registers
|
||||
|
@ -1146,4 +1245,18 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
|||
end
|
||||
"""
|
||||
end
|
||||
|
||||
defp clean_message(message) do
|
||||
message
|
||||
|> remove_trailing_whitespace()
|
||||
|> remove_ansi()
|
||||
end
|
||||
|
||||
defp remove_trailing_whitespace(string) do
|
||||
String.replace(string, ~r/ +$/m, "")
|
||||
end
|
||||
|
||||
defp remove_ansi(string) do
|
||||
String.replace(string, ~r/\e\[\d+m/, "")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -21,7 +21,7 @@ defmodule Livebook.Runtime.NoopRuntime do
|
|||
def disconnect(runtime), do: {:ok, %{runtime | started: false}}
|
||||
def duplicate(_), do: Livebook.Runtime.NoopRuntime.new()
|
||||
|
||||
def evaluate_code(_, _, _, _, _ \\ []), do: :ok
|
||||
def evaluate_code(_, _, _, _, _, _ \\ []), do: :ok
|
||||
def forget_evaluation(_, _), do: :ok
|
||||
def drop_container(_, _), do: :ok
|
||||
def handle_intellisense(_, _, _, _), do: make_ref()
|
||||
|
|
Loading…
Add table
Reference in a new issue