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:
Benedikt Reinartz 2023-06-02 14:49:06 +02:00 committed by GitHub
parent 14dd6d925f
commit fb9193b8ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 1033 additions and 436 deletions

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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

View file

@ -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}

View file

@ -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(),

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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}

View file

@ -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()

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 = "![](#{url})"
{: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

View file

@ -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

View file

@ -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">

View file

@ -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"

View 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>

View file

@ -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>

View file

@ -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")

View file

@ -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")

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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()