mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-10-11 14:06:20 +08:00
Changes for Elixir 1.15 (#1918)
This commit is contained in:
parent
cab24f2a8c
commit
d2988f985d
19 changed files with 454 additions and 162 deletions
10
.github/workflows/deploy.yml
vendored
10
.github/workflows/deploy.yml
vendored
|
@ -6,8 +6,8 @@ on:
|
||||||
tags:
|
tags:
|
||||||
- "v*.*.*"
|
- "v*.*.*"
|
||||||
env:
|
env:
|
||||||
otp: "25.0"
|
otp: "26.0"
|
||||||
elixir: "1.14.2"
|
elixir: "1.15.0-rc.1"
|
||||||
jobs:
|
jobs:
|
||||||
assets:
|
assets:
|
||||||
outputs:
|
outputs:
|
||||||
|
@ -66,9 +66,9 @@ jobs:
|
||||||
# we pass an empty ref and and the default is used
|
# we pass an empty ref and and the default is used
|
||||||
ref: ${{ needs.assets.outputs.sha }}
|
ref: ${{ needs.assets.outputs.sha }}
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v2
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v2
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
|
@ -84,7 +84,7 @@ jobs:
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=edge,branch=main
|
type=edge,branch=main
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
|
@ -5,8 +5,8 @@ on:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
env:
|
env:
|
||||||
otp: "25.0.4"
|
otp: "26.0"
|
||||||
elixir: "1.14.2"
|
elixir: "1.15.0-rc.1"
|
||||||
jobs:
|
jobs:
|
||||||
main:
|
main:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
4
.github/workflows/uffizzi-build.yml
vendored
4
.github/workflows/uffizzi-build.yml
vendored
|
@ -4,8 +4,8 @@ on:
|
||||||
types: [opened, synchronize, reopened, closed]
|
types: [opened, synchronize, reopened, closed]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
otp: "25.0"
|
otp: "26.0"
|
||||||
elixir: "1.14.2"
|
elixir: "1.15.0-rc.1"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-application:
|
build-application:
|
||||||
|
|
|
@ -238,8 +238,8 @@ const Cell = {
|
||||||
|
|
||||||
this.handleEvent(
|
this.handleEvent(
|
||||||
`evaluation_finished:${this.props.cellId}`,
|
`evaluation_finished:${this.props.cellId}`,
|
||||||
({ code_error }) => {
|
({ code_markers }) => {
|
||||||
liveEditor.setCodeErrorMarker(code_error);
|
liveEditor.setCodeMarkers(code_markers);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -210,33 +210,34 @@ class LiveEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds an underline marker for the given syntax error.
|
* Sets underline markers for warnings and errors.
|
||||||
*
|
*
|
||||||
* To clear an existing marker `null` error is also supported.
|
* Passing an empty list clears all markers.
|
||||||
*/
|
*/
|
||||||
setCodeErrorMarker(error) {
|
setCodeMarkers(codeMarkers) {
|
||||||
this._ensureMounted();
|
this._ensureMounted();
|
||||||
|
|
||||||
const owner = "livebook.error.syntax";
|
const owner = "livebook.code-marker";
|
||||||
|
|
||||||
if (error) {
|
const editorMarkers = codeMarkers.map((codeMarker) => {
|
||||||
const line = this.editor.getModel().getLineContent(error.line);
|
const line = this.editor.getModel().getLineContent(codeMarker.line);
|
||||||
const [, leadingWhitespace, trailingWhitespace] =
|
const [, leadingWhitespace, trailingWhitespace] =
|
||||||
line.match(/^(\s*).*?(\s*)$/);
|
line.match(/^(\s*).*?(\s*)$/);
|
||||||
|
|
||||||
monaco.editor.setModelMarkers(this.editor.getModel(), owner, [
|
return {
|
||||||
{
|
startLineNumber: codeMarker.line,
|
||||||
startLineNumber: error.line,
|
|
||||||
startColumn: leadingWhitespace.length + 1,
|
startColumn: leadingWhitespace.length + 1,
|
||||||
endLineNumber: error.line,
|
endLineNumber: codeMarker.line,
|
||||||
endColumn: line.length + 1 - trailingWhitespace.length,
|
endColumn: line.length + 1 - trailingWhitespace.length,
|
||||||
message: error.description,
|
message: codeMarker.description,
|
||||||
severity: monaco.MarkerSeverity.Error,
|
severity: {
|
||||||
},
|
error: monaco.MarkerSeverity.Error,
|
||||||
]);
|
warning: monaco.MarkerSeverity.Warning,
|
||||||
} else {
|
}[codeMarker.severity],
|
||||||
monaco.editor.setModelMarkers(this.editor.getModel(), owner, []);
|
};
|
||||||
}
|
});
|
||||||
|
|
||||||
|
monaco.editor.setModelMarkers(this.editor.getModel(), owner, editorMarkers);
|
||||||
}
|
}
|
||||||
|
|
||||||
_mountEditor() {
|
_mountEditor() {
|
||||||
|
@ -506,7 +507,7 @@ class LiveEditor {
|
||||||
|
|
||||||
return this._asyncIntellisenseRequest("format", { code: content })
|
return this._asyncIntellisenseRequest("format", { code: content })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
this.setCodeErrorMarker(response.code_error);
|
this.setCodeMarkers(response.code_markers);
|
||||||
|
|
||||||
if (response.code) {
|
if (response.code) {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -65,11 +65,11 @@ defmodule Livebook.Intellisense do
|
||||||
|> Code.format_string!()
|
|> Code.format_string!()
|
||||||
|> IO.iodata_to_binary()
|
|> IO.iodata_to_binary()
|
||||||
|
|
||||||
%{code: formatted, code_error: nil}
|
%{code: formatted, code_markers: []}
|
||||||
rescue
|
rescue
|
||||||
error ->
|
error ->
|
||||||
code_error = %{line: error.line, description: error.description}
|
code_marker = %{line: error.line, description: error.description, severity: :error}
|
||||||
%{code: nil, code_error: code_error}
|
%{code: nil, code_markers: [code_marker]}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -148,6 +148,15 @@ defmodule Livebook.Intellisense do
|
||||||
insert_text: Atom.to_string(name)
|
insert_text: Atom.to_string(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defp format_completion_item(%{kind: :in_map_field, name: name}),
|
||||||
|
do: %{
|
||||||
|
label: Atom.to_string(name),
|
||||||
|
kind: :field,
|
||||||
|
detail: "field",
|
||||||
|
documentation: nil,
|
||||||
|
insert_text: "#{name}: "
|
||||||
|
}
|
||||||
|
|
||||||
defp format_completion_item(%{
|
defp format_completion_item(%{
|
||||||
kind: :in_struct_field,
|
kind: :in_struct_field,
|
||||||
struct: struct,
|
struct: struct,
|
||||||
|
@ -413,6 +422,8 @@ defmodule Livebook.Intellisense do
|
||||||
|
|
||||||
defp format_details_item(%{kind: :map_field, name: name}), do: code(name)
|
defp format_details_item(%{kind: :map_field, name: name}), do: code(name)
|
||||||
|
|
||||||
|
defp format_details_item(%{kind: :in_map_field, name: name}), do: code(name)
|
||||||
|
|
||||||
defp format_details_item(%{kind: :in_struct_field, name: name, default: default}) do
|
defp format_details_item(%{kind: :in_struct_field, name: name, default: default}) do
|
||||||
join_with_divider([
|
join_with_divider([
|
||||||
code(name),
|
code(name),
|
||||||
|
|
|
@ -28,6 +28,10 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
||||||
kind: :map_field,
|
kind: :map_field,
|
||||||
name: name()
|
name: name()
|
||||||
}
|
}
|
||||||
|
| %{
|
||||||
|
kind: :in_map_field,
|
||||||
|
name: name()
|
||||||
|
}
|
||||||
| %{
|
| %{
|
||||||
kind: :in_struct_field,
|
kind: :in_struct_field,
|
||||||
module: module(),
|
module: module(),
|
||||||
|
@ -94,6 +98,9 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
||||||
{:utf32, 0}
|
{:utf32, 0}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@alias_only_atoms ~w(alias import require)a
|
||||||
|
@alias_only_charlists ~w(alias import require)c
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns a list of identifiers matching the given `hint`
|
Returns a list of identifiers matching the given `hint`
|
||||||
together with relevant information.
|
together with relevant information.
|
||||||
|
@ -161,7 +168,11 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
||||||
match_erlang_module(List.to_string(unquoted_atom), ctx)
|
match_erlang_module(List.to_string(unquoted_atom), ctx)
|
||||||
|
|
||||||
{:dot, path, hint} ->
|
{:dot, path, hint} ->
|
||||||
|
if alias = dot_alias_only(path, hint, ctx.fragment, ctx) do
|
||||||
|
match_alias(List.to_string(alias), ctx, false)
|
||||||
|
else
|
||||||
match_dot(path, List.to_string(hint), ctx)
|
match_dot(path, List.to_string(hint), ctx)
|
||||||
|
end
|
||||||
|
|
||||||
{:dot_arity, path, hint} ->
|
{:dot_arity, path, hint} ->
|
||||||
match_dot(path, List.to_string(hint), %{ctx | matcher: @exact_matcher})
|
match_dot(path, List.to_string(hint), %{ctx | matcher: @exact_matcher})
|
||||||
|
@ -170,17 +181,18 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
||||||
match_default(ctx)
|
match_default(ctx)
|
||||||
|
|
||||||
:expr ->
|
:expr ->
|
||||||
match_default(ctx)
|
match_container_context(ctx.fragment, :expr, "", ctx) || match_default(ctx)
|
||||||
|
|
||||||
{:local_or_var, local_or_var} ->
|
{:local_or_var, local_or_var} ->
|
||||||
hint = List.to_string(local_or_var)
|
hint = List.to_string(local_or_var)
|
||||||
|
match_container_context(ctx.fragment, :expr, hint, ctx) || match_local_or_var(hint, ctx)
|
||||||
match_container_context(ctx.fragment, hint) ||
|
|
||||||
match_in_struct_fields_or_local_or_var(hint, ctx)
|
|
||||||
|
|
||||||
{:local_arity, local} ->
|
{:local_arity, local} ->
|
||||||
match_local(List.to_string(local), %{ctx | matcher: @exact_matcher})
|
match_local(List.to_string(local), %{ctx | matcher: @exact_matcher})
|
||||||
|
|
||||||
|
{:local_call, local} when local in @alias_only_charlists ->
|
||||||
|
match_alias("", ctx, false)
|
||||||
|
|
||||||
{:local_call, local} ->
|
{:local_call, local} ->
|
||||||
case ctx.type do
|
case ctx.type do
|
||||||
:completion -> match_default(ctx)
|
:completion -> match_default(ctx)
|
||||||
|
@ -188,7 +200,7 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
||||||
end
|
end
|
||||||
|
|
||||||
{:operator, operator} when operator in ~w(:: -)c ->
|
{:operator, operator} when operator in ~w(:: -)c ->
|
||||||
match_container_context(ctx.fragment, "") ||
|
match_container_context(ctx.fragment, :operator, "", ctx) ||
|
||||||
match_local_or_var(List.to_string(operator), ctx)
|
match_local_or_var(List.to_string(operator), ctx)
|
||||||
|
|
||||||
{:operator, operator} ->
|
{:operator, operator} ->
|
||||||
|
@ -197,6 +209,9 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
||||||
{:operator_arity, operator} ->
|
{:operator_arity, operator} ->
|
||||||
match_local(List.to_string(operator), %{ctx | matcher: @exact_matcher})
|
match_local(List.to_string(operator), %{ctx | matcher: @exact_matcher})
|
||||||
|
|
||||||
|
{:operator_call, operator} when operator in ~w(|)c ->
|
||||||
|
match_container_context(ctx.fragment, :expr, "", ctx) || match_default(ctx)
|
||||||
|
|
||||||
{:operator_call, _operator} ->
|
{:operator_call, _operator} ->
|
||||||
match_default(ctx)
|
match_default(ctx)
|
||||||
|
|
||||||
|
@ -280,7 +295,7 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp match_default(ctx) do
|
defp match_default(ctx) do
|
||||||
match_in_struct_fields_or_local_or_var("", ctx)
|
match_local_or_var("", ctx)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp match_alias(hint, ctx, nested?) do
|
defp match_alias(hint, ctx, nested?) do
|
||||||
|
@ -294,6 +309,16 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp dot_alias_only(path, hint, code, ctx) do
|
||||||
|
with {:alias, alias} <- path,
|
||||||
|
[] <- hint,
|
||||||
|
:alias_only <- container_context(code, ctx) do
|
||||||
|
alias ++ [?.]
|
||||||
|
else
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp match_struct(hint, ctx) do
|
defp match_struct(hint, ctx) do
|
||||||
for %{kind: :module, module: module} = item <- match_alias(hint, ctx, true),
|
for %{kind: :module, module: module} = item <- match_alias(hint, ctx, true),
|
||||||
has_struct?(module),
|
has_struct?(module),
|
||||||
|
@ -313,21 +338,14 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
||||||
match_module_function(mod, hint, ctx) ++ match_module_type(mod, hint, ctx)
|
match_module_function(mod, hint, ctx) ++ match_module_type(mod, hint, ctx)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp match_in_struct_fields_or_local_or_var(hint, ctx) do
|
defp match_container_context(code, context, hint, ctx) do
|
||||||
case expand_struct_fields(ctx) do
|
case container_context(code, ctx) do
|
||||||
{:ok, struct, fields} ->
|
{:map, map, pairs} when context == :expr ->
|
||||||
for {field, default} <- fields,
|
container_context_map_fields(pairs, map, hint, ctx)
|
||||||
name = Atom.to_string(field),
|
|
||||||
ctx.matcher.(name, hint),
|
|
||||||
do: %{kind: :in_struct_field, struct: struct, name: field, default: default}
|
|
||||||
|
|
||||||
_ ->
|
{:struct, alias, pairs} when context == :expr ->
|
||||||
match_local_or_var(hint, ctx)
|
container_context_struct_fields(pairs, alias, hint, ctx)
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp match_container_context(code, hint) do
|
|
||||||
case container_context(code) do
|
|
||||||
:bitstring_modifier ->
|
:bitstring_modifier ->
|
||||||
existing = code |> String.split("::") |> List.last() |> String.split("-")
|
existing = code |> String.split("::") |> List.last() |> String.split("-")
|
||||||
|
|
||||||
|
@ -341,55 +359,97 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp container_context(code) do
|
defp container_context(code, ctx) do
|
||||||
case Code.Fragment.container_cursor_to_quoted(code) do
|
case Code.Fragment.container_cursor_to_quoted(code) do
|
||||||
{:ok, quoted} ->
|
{:ok, quoted} ->
|
||||||
case Macro.path(quoted, &match?({:__cursor__, _, []}, &1)) do
|
case Macro.path(quoted, &match?({:__cursor__, _, []}, &1)) do
|
||||||
[cursor, {:"::", _, [_, cursor]}, {:<<>>, _, [_ | _]} | _] -> :bitstring_modifier
|
[cursor, {:%{}, _, pairs}, {:%, _, [{:__aliases__, _, aliases}, _map]} | _] ->
|
||||||
|
container_context_struct(cursor, pairs, aliases, ctx)
|
||||||
|
|
||||||
|
[
|
||||||
|
cursor,
|
||||||
|
pairs,
|
||||||
|
{:|, _, _},
|
||||||
|
{:%{}, _, _},
|
||||||
|
{:%, _, [{:__aliases__, _, aliases}, _map]} | _
|
||||||
|
] ->
|
||||||
|
container_context_struct(cursor, pairs, aliases, ctx)
|
||||||
|
|
||||||
|
[cursor, pairs, {:|, _, [{variable, _, nil} | _]}, {:%{}, _, _} | _] ->
|
||||||
|
container_context_map(cursor, pairs, variable, ctx)
|
||||||
|
|
||||||
|
[cursor, {special_form, _, [cursor]} | _] when special_form in @alias_only_atoms ->
|
||||||
|
:alias_only
|
||||||
|
|
||||||
|
[cursor | tail] ->
|
||||||
|
case remove_operators(tail, cursor) do
|
||||||
|
[{:"::", _, [_, _]}, {:<<>>, _, [_ | _]} | _] -> :bitstring_modifier
|
||||||
_ -> nil
|
_ -> nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
{:error, _} ->
|
{:error, _} ->
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp expand_struct_fields(ctx) do
|
defp remove_operators([{op, _, [_, previous]} = head | tail], previous) when op in [:-],
|
||||||
with {:ok, quoted} <- Code.Fragment.container_cursor_to_quoted(ctx.fragment),
|
do: remove_operators(tail, head)
|
||||||
{aliases, pairs} <- find_struct_fields(quoted) do
|
|
||||||
mod_name = Enum.join(aliases, ".")
|
|
||||||
mod = expand_alias(mod_name, ctx)
|
|
||||||
|
|
||||||
fields =
|
defp remove_operators(tail, _previous),
|
||||||
if has_struct?(mod) do
|
do: tail
|
||||||
|
|
||||||
|
defp container_context_struct(cursor, pairs, aliases, ctx) do
|
||||||
|
with {pairs, [^cursor]} <- Enum.split(pairs, -1),
|
||||||
|
alias = expand_alias(aliases, ctx),
|
||||||
|
true <- Keyword.keyword?(pairs) and has_struct?(alias) do
|
||||||
|
{:struct, alias, pairs}
|
||||||
|
else
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp container_context_map(cursor, pairs, variable, ctx) do
|
||||||
|
with {pairs, [^cursor]} <- Enum.split(pairs, -1),
|
||||||
|
{:ok, map} when is_map(map) <- value_from_binding([variable], ctx),
|
||||||
|
true <- Keyword.keyword?(pairs) do
|
||||||
|
{:map, map, pairs}
|
||||||
|
else
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp container_context_map_fields(pairs, map, hint, ctx) do
|
||||||
|
map = filter_out_fields(map, pairs)
|
||||||
|
|
||||||
|
for {key, _value} <- map,
|
||||||
|
name = Atom.to_string(key),
|
||||||
|
ctx.matcher.(name, hint),
|
||||||
|
do: %{kind: :in_map_field, name: key}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp container_context_struct_fields(pairs, mod, hint, ctx) do
|
||||||
|
map = Map.from_struct(mod.__struct__)
|
||||||
|
map = filter_out_fields(map, pairs)
|
||||||
|
|
||||||
|
for {field, default} <- map,
|
||||||
|
name = Atom.to_string(field),
|
||||||
|
ctx.matcher.(name, hint),
|
||||||
|
do: %{kind: :in_struct_field, struct: mod, name: field, default: default}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp filter_out_fields(map, pairs) do
|
||||||
# Remove the keys that have already been filled, and internal keys
|
# Remove the keys that have already been filled, and internal keys
|
||||||
Map.from_struct(mod.__struct__)
|
map
|
||||||
|> Map.drop(Keyword.keys(pairs))
|
|> Map.drop(Keyword.keys(pairs))
|
||||||
|> Map.reject(fn {key, _} ->
|
|> Map.reject(fn {key, _} ->
|
||||||
key
|
key
|
||||||
|> Atom.to_string()
|
|> Atom.to_string()
|
||||||
|> String.starts_with?("_")
|
|> String.starts_with?("_")
|
||||||
end)
|
end)
|
||||||
else
|
|
||||||
%{}
|
|
||||||
end
|
|
||||||
|
|
||||||
{:ok, mod, fields}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp find_struct_fields(ast) do
|
|
||||||
ast
|
|
||||||
|> Macro.prewalker()
|
|
||||||
|> Enum.find_value(fn node ->
|
|
||||||
with {:%, _, [{:__aliases__, _, aliases}, {:%{}, _, pairs}]} <- node,
|
|
||||||
{pairs, [{:__cursor__, _, []}]} <- Enum.split(pairs, -1),
|
|
||||||
true <- Keyword.keyword?(pairs) do
|
|
||||||
{aliases, pairs}
|
|
||||||
else
|
|
||||||
_ -> nil
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp match_local_or_var(hint, ctx) do
|
defp match_local_or_var(hint, ctx) do
|
||||||
|
@ -445,9 +505,14 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
||||||
end
|
end
|
||||||
|
|
||||||
# Converts alias string to module atom with regard to the given env
|
# Converts alias string to module atom with regard to the given env
|
||||||
defp expand_alias(alias, ctx) do
|
defp expand_alias(alias, ctx) when is_binary(alias) do
|
||||||
[name | rest] = alias |> String.split(".") |> Enum.map(&String.to_atom/1)
|
alias
|
||||||
|
|> String.split(".")
|
||||||
|
|> Enum.map(&String.to_atom/1)
|
||||||
|
|> expand_alias(ctx)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp expand_alias([name | rest], ctx) do
|
||||||
case Macro.Env.fetch_alias(ctx.intellisense_context.env, name) do
|
case Macro.Env.fetch_alias(ctx.intellisense_context.env, name) do
|
||||||
{:ok, name} when rest == [] -> name
|
{:ok, name} when rest == [] -> name
|
||||||
{:ok, name} -> Module.concat([name | rest])
|
{:ok, name} -> Module.concat([name | rest])
|
||||||
|
|
|
@ -250,7 +250,7 @@ defmodule Livebook.LiveMarkdown.Export do
|
||||||
defp render_output(_output, _ctx), do: :ignored
|
defp render_output(_output, _ctx), do: :ignored
|
||||||
|
|
||||||
defp encode_js_data(data) when is_binary(data), do: {:ok, data}
|
defp encode_js_data(data) when is_binary(data), do: {:ok, data}
|
||||||
defp encode_js_data(data), do: Jason.encode(data)
|
defp encode_js_data(data), do: data |> ensure_order() |> Jason.encode()
|
||||||
|
|
||||||
defp get_code_cell_code(%{source: source, disable_formatting: true}),
|
defp get_code_cell_code(%{source: source, disable_formatting: true}),
|
||||||
do: source
|
do: source
|
||||||
|
@ -258,7 +258,7 @@ defmodule Livebook.LiveMarkdown.Export do
|
||||||
defp get_code_cell_code(%{source: source}), do: format_code(source)
|
defp get_code_cell_code(%{source: source}), do: format_code(source)
|
||||||
|
|
||||||
defp render_metadata(metadata) do
|
defp render_metadata(metadata) do
|
||||||
metadata_json = Jason.encode!(metadata)
|
metadata_json = metadata |> ensure_order() |> Jason.encode!()
|
||||||
["<!-- livebook:", metadata_json, " -->"]
|
["<!-- livebook:", metadata_json, " -->"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -333,7 +333,7 @@ defmodule Livebook.LiveMarkdown.Export do
|
||||||
with {:ok, hub} <- Livebook.Hubs.fetch_hub(notebook.hub_id),
|
with {:ok, hub} <- Livebook.Hubs.fetch_hub(notebook.hub_id),
|
||||||
{:ok, stamp} <- Livebook.Hubs.notebook_stamp(hub, notebook_source, metadata) do
|
{:ok, stamp} <- Livebook.Hubs.notebook_stamp(hub, notebook_source, metadata) do
|
||||||
offset = IO.iodata_length(notebook_source)
|
offset = IO.iodata_length(notebook_source)
|
||||||
json = Jason.encode!(%{"offset" => offset, "stamp" => stamp})
|
json = %{"offset" => offset, "stamp" => stamp} |> ensure_order() |> Jason.encode!()
|
||||||
["\n", "<!-- livebook:", json, " -->", "\n"]
|
["\n", "<!-- livebook:", json, " -->", "\n"]
|
||||||
else
|
else
|
||||||
_ -> []
|
_ -> []
|
||||||
|
@ -344,4 +344,14 @@ defmodule Livebook.LiveMarkdown.Export do
|
||||||
keys = [:hub_secret_names]
|
keys = [:hub_secret_names]
|
||||||
put_unless_default(%{}, Map.take(notebook, keys), Map.take(Notebook.new(), keys))
|
put_unless_default(%{}, Map.take(notebook, keys), Map.take(Notebook.new(), keys))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp ensure_order(map) do
|
||||||
|
map
|
||||||
|
|> Enum.sort()
|
||||||
|
|> Enum.map(fn
|
||||||
|
{key, %{} = value} -> {key, ensure_order(value)}
|
||||||
|
pair -> pair
|
||||||
|
end)
|
||||||
|
|> Jason.OrderedObject.new()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -230,14 +230,12 @@ setup cell to invoke `Mix.install/2` with the following arguments:
|
||||||
<!-- livebook:{"force_markdown":true} -->
|
<!-- livebook:{"force_markdown":true} -->
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
my_app_root = Path.join(__DIR__, "..")
|
|
||||||
|
|
||||||
Mix.install(
|
Mix.install(
|
||||||
[
|
[
|
||||||
{:my_app, path: my_app_root, env: :dev}
|
{:my_app, path: Path.join(__DIR__, ".."), env: :dev}
|
||||||
],
|
],
|
||||||
config_path: Path.join(my_app_root, "config/config.exs"),
|
config_path: :my_app,
|
||||||
lockfile: Path.join(my_app_root, "mix.lock")
|
lockfile: :my_app
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -86,7 +86,7 @@ defprotocol Livebook.Runtime do
|
||||||
@type evaluation_response_metadata :: %{
|
@type evaluation_response_metadata :: %{
|
||||||
errored: boolean(),
|
errored: boolean(),
|
||||||
evaluation_time_ms: non_neg_integer(),
|
evaluation_time_ms: non_neg_integer(),
|
||||||
code_error: code_error(),
|
code_markers: list(code_marker()),
|
||||||
memory_usage: runtime_memory(),
|
memory_usage: runtime_memory(),
|
||||||
identifiers_used: list(identifier :: term()) | :unknown,
|
identifiers_used: list(identifier :: term()) | :unknown,
|
||||||
identifiers_defined: %{(identifier :: term()) => version :: term()}
|
identifiers_defined: %{(identifier :: term()) => version :: term()}
|
||||||
|
@ -175,13 +175,17 @@ defprotocol Livebook.Runtime do
|
||||||
|
|
||||||
@type format_response :: %{
|
@type format_response :: %{
|
||||||
code: String.t() | nil,
|
code: String.t() | nil,
|
||||||
code_error: code_error() | nil
|
code_markers: list(code_marker())
|
||||||
}
|
}
|
||||||
|
|
||||||
@typedoc """
|
@typedoc """
|
||||||
A descriptive error pointing to a specific line in the code.
|
A descriptive error or warning pointing to a specific line in the code.
|
||||||
"""
|
"""
|
||||||
@type code_error :: %{line: pos_integer(), description: String.t()}
|
@type code_marker :: %{
|
||||||
|
line: pos_integer(),
|
||||||
|
description: String.t(),
|
||||||
|
severity: :error | :warning
|
||||||
|
}
|
||||||
|
|
||||||
@typedoc """
|
@typedoc """
|
||||||
A detailed runtime memory usage.
|
A detailed runtime memory usage.
|
||||||
|
|
|
@ -38,6 +38,7 @@ defmodule Livebook.Runtime.ErlDist do
|
||||||
Livebook.Runtime.ErlDist.EvaluatorSupervisor,
|
Livebook.Runtime.ErlDist.EvaluatorSupervisor,
|
||||||
Livebook.Runtime.ErlDist.IOForwardGL,
|
Livebook.Runtime.ErlDist.IOForwardGL,
|
||||||
Livebook.Runtime.ErlDist.LoggerGLBackend,
|
Livebook.Runtime.ErlDist.LoggerGLBackend,
|
||||||
|
Livebook.Runtime.ErlDist.LoggerGLHandler,
|
||||||
Livebook.Runtime.ErlDist.SmartCellGL
|
Livebook.Runtime.ErlDist.SmartCellGL
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
27
lib/livebook/runtime/erl_dist/logger_gl_handler.ex
Normal file
27
lib/livebook/runtime/erl_dist/logger_gl_handler.ex
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
defmodule Livebook.Runtime.ErlDist.LoggerGLHandler do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def log(%{meta: meta} = event, %{formatter: {formatter_module, formatter_config}}) do
|
||||||
|
message = apply(formatter_module, :format, [event, formatter_config])
|
||||||
|
|
||||||
|
if io_proxy?(meta.gl) do
|
||||||
|
async_io(meta.gl, message)
|
||||||
|
else
|
||||||
|
send(Livebook.Runtime.ErlDist.NodeManager, {:orphan_log, message})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp io_proxy?(pid) do
|
||||||
|
try do
|
||||||
|
info = Process.info(pid, [:dictionary])
|
||||||
|
info[:dictionary][:"$initial_call"] == {Livebook.Runtime.Evaluator.IOProxy, :init, 1}
|
||||||
|
rescue
|
||||||
|
_ -> false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def async_io(device, output) when is_pid(device) do
|
||||||
|
send(device, {:io_request, self(), make_ref(), {:put_chars, :unicode, output}})
|
||||||
|
end
|
||||||
|
end
|
|
@ -103,7 +103,14 @@ defmodule Livebook.Runtime.ErlDist.NodeManager do
|
||||||
Process.unregister(:standard_error)
|
Process.unregister(:standard_error)
|
||||||
Process.register(io_forward_gl_pid, :standard_error)
|
Process.register(io_forward_gl_pid, :standard_error)
|
||||||
|
|
||||||
|
# TODO: remove logger backend once we require Elixir v1.15
|
||||||
|
if Code.ensure_loaded?(Logger) and function_exported?(Logger, :add_handlers, 1) do
|
||||||
|
:logger.add_handler(:livebook_gl_handler, Livebook.Runtime.ErlDist.LoggerGLHandler, %{
|
||||||
|
formatter: Logger.Formatter.new()
|
||||||
|
})
|
||||||
|
else
|
||||||
Logger.add_backend(Livebook.Runtime.ErlDist.LoggerGLBackend)
|
Logger.add_backend(Livebook.Runtime.ErlDist.LoggerGLBackend)
|
||||||
|
end
|
||||||
|
|
||||||
# Set `ignore_module_conflict` only for the NodeManager lifetime.
|
# Set `ignore_module_conflict` only for the NodeManager lifetime.
|
||||||
initial_ignore_module_conflict = Code.compiler_options()[:ignore_module_conflict]
|
initial_ignore_module_conflict = Code.compiler_options()[:ignore_module_conflict]
|
||||||
|
@ -152,7 +159,12 @@ defmodule Livebook.Runtime.ErlDist.NodeManager do
|
||||||
Process.unregister(:standard_error)
|
Process.unregister(:standard_error)
|
||||||
Process.register(state.original_standard_error, :standard_error)
|
Process.register(state.original_standard_error, :standard_error)
|
||||||
|
|
||||||
|
# TODO: remove logger backend once we require Elixir v1.15
|
||||||
|
if Code.ensure_loaded?(Logger) and function_exported?(Logger, :add_handlers, 1) do
|
||||||
|
:logger.remove_handler(:livebook_gl_handler)
|
||||||
|
else
|
||||||
Logger.remove_backend(Livebook.Runtime.ErlDist.LoggerGLBackend)
|
Logger.remove_backend(Livebook.Runtime.ErlDist.LoggerGLBackend)
|
||||||
|
end
|
||||||
|
|
||||||
if state.unload_modules_on_termination do
|
if state.unload_modules_on_termination do
|
||||||
ErlDist.unload_required_modules()
|
ErlDist.unload_required_modules()
|
||||||
|
|
|
@ -419,12 +419,12 @@ defmodule Livebook.Runtime.Evaluator do
|
||||||
set_pdict(context, state.ignored_pdict_keys)
|
set_pdict(context, state.ignored_pdict_keys)
|
||||||
|
|
||||||
start_time = System.monotonic_time()
|
start_time = System.monotonic_time()
|
||||||
eval_result = eval(code, context.binding, context.env)
|
{eval_result, code_markers} = eval(code, context.binding, context.env)
|
||||||
evaluation_time_ms = time_diff_ms(start_time)
|
evaluation_time_ms = time_diff_ms(start_time)
|
||||||
|
|
||||||
%{tracer_info: tracer_info} = Evaluator.IOProxy.after_evaluation(state.io_proxy)
|
%{tracer_info: tracer_info} = Evaluator.IOProxy.after_evaluation(state.io_proxy)
|
||||||
|
|
||||||
{new_context, result, code_error, identifiers_used, identifiers_defined} =
|
{new_context, result, identifiers_used, identifiers_defined} =
|
||||||
case eval_result do
|
case eval_result do
|
||||||
{:ok, value, binding, env} ->
|
{:ok, value, binding, env} ->
|
||||||
context_id = random_id()
|
context_id = random_id()
|
||||||
|
@ -440,9 +440,9 @@ defmodule Livebook.Runtime.Evaluator do
|
||||||
identifier_dependencies(new_context, tracer_info, context)
|
identifier_dependencies(new_context, tracer_info, context)
|
||||||
|
|
||||||
result = {:ok, value}
|
result = {:ok, value}
|
||||||
{new_context, result, nil, identifiers_used, identifiers_defined}
|
{new_context, result, identifiers_used, identifiers_defined}
|
||||||
|
|
||||||
{:error, kind, error, stacktrace, code_error} ->
|
{:error, kind, error, stacktrace} ->
|
||||||
for {module, _} <- tracer_info.modules_defined do
|
for {module, _} <- tracer_info.modules_defined do
|
||||||
delete_module(module)
|
delete_module(module)
|
||||||
end
|
end
|
||||||
|
@ -452,7 +452,7 @@ defmodule Livebook.Runtime.Evaluator do
|
||||||
identifiers_defined = %{}
|
identifiers_defined = %{}
|
||||||
# Empty context
|
# Empty context
|
||||||
new_context = initial_context()
|
new_context = initial_context()
|
||||||
{new_context, result, code_error, identifiers_used, identifiers_defined}
|
{new_context, result, identifiers_used, identifiers_defined}
|
||||||
end
|
end
|
||||||
|
|
||||||
if ebin_path() do
|
if ebin_path() do
|
||||||
|
@ -474,7 +474,7 @@ defmodule Livebook.Runtime.Evaluator do
|
||||||
),
|
),
|
||||||
evaluation_time_ms: evaluation_time_ms,
|
evaluation_time_ms: evaluation_time_ms,
|
||||||
memory_usage: memory(),
|
memory_usage: memory(),
|
||||||
code_error: code_error,
|
code_markers: code_markers,
|
||||||
identifiers_used: identifiers_used,
|
identifiers_used: identifiers_used,
|
||||||
identifiers_defined: identifiers_defined
|
identifiers_defined: identifiers_defined
|
||||||
}
|
}
|
||||||
|
@ -603,32 +603,91 @@ defmodule Livebook.Runtime.Evaluator do
|
||||||
|> Map.update!(:context_modules, &(&1 ++ prev_env.context_modules))
|
|> Map.update!(:context_modules, &(&1 ++ prev_env.context_modules))
|
||||||
end
|
end
|
||||||
|
|
||||||
@compile {:no_warn_undefined, {Code, :eval_quoted_with_env, 4}}
|
|
||||||
|
|
||||||
defp eval(code, binding, env) do
|
defp eval(code, binding, env) do
|
||||||
|
{{result, extra_diagnostics}, diagnostics} =
|
||||||
|
with_diagnostics([log: true], fn ->
|
||||||
try do
|
try do
|
||||||
quoted = Code.string_to_quoted!(code, file: env.file)
|
quoted = Code.string_to_quoted!(code, file: env.file)
|
||||||
{value, binding, env} = Code.eval_quoted_with_env(quoted, binding, env, prune_binding: true)
|
|
||||||
|
try do
|
||||||
|
{value, binding, env} =
|
||||||
|
Code.eval_quoted_with_env(quoted, binding, env, prune_binding: true)
|
||||||
|
|
||||||
{:ok, value, binding, env}
|
{:ok, value, binding, env}
|
||||||
catch
|
catch
|
||||||
kind, error ->
|
kind, error ->
|
||||||
stacktrace = prune_stacktrace(__STACKTRACE__)
|
stacktrace = prune_stacktrace(__STACKTRACE__)
|
||||||
|
{:error, kind, error, stacktrace}
|
||||||
|
end
|
||||||
|
catch
|
||||||
|
kind, error ->
|
||||||
|
{:error, kind, error, []}
|
||||||
|
end
|
||||||
|
|> case do
|
||||||
|
{:ok, value, binding, env} ->
|
||||||
|
{{:ok, value, binding, env}, []}
|
||||||
|
|
||||||
code_error =
|
{:error, kind, error, stacktrace} ->
|
||||||
if code_error?(error) and (error.file == env.file and error.file != "nofile") do
|
# Mimic a diagnostic for relevant errors where it's not
|
||||||
%{line: error.line, description: error.description}
|
# the case by default
|
||||||
|
extra_diagnostics =
|
||||||
|
if extra_diagnostic?(error) do
|
||||||
|
[
|
||||||
|
%{
|
||||||
|
file: error.file,
|
||||||
|
severity: :error,
|
||||||
|
message: error.description,
|
||||||
|
position: error.line,
|
||||||
|
stacktrace: stacktrace
|
||||||
|
}
|
||||||
|
]
|
||||||
else
|
else
|
||||||
nil
|
[]
|
||||||
end
|
end
|
||||||
|
|
||||||
{:error, kind, error, stacktrace, code_error}
|
{{:error, kind, error, stacktrace}, extra_diagnostics}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
code_markers =
|
||||||
|
for diagnostic <- diagnostics ++ extra_diagnostics,
|
||||||
|
# Ignore diagnostics from other evaluations, such as inner Code.eval_string/3
|
||||||
|
diagnostic.file == env.file and diagnostic.file != "nofile" do
|
||||||
|
%{
|
||||||
|
line:
|
||||||
|
case diagnostic.position do
|
||||||
|
{line, _column} -> line
|
||||||
|
line -> line
|
||||||
|
end,
|
||||||
|
description: diagnostic.message,
|
||||||
|
severity: diagnostic.severity
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
{result, code_markers}
|
||||||
|
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
|
||||||
|
Code.with_diagnostics(opts, fun)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
defp with_diagnostics(_opts, fun) do
|
||||||
|
{fun.(), []}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp code_error?(%SyntaxError{}), do: true
|
defp extra_diagnostic?(%SyntaxError{}), do: true
|
||||||
defp code_error?(%TokenMissingError{}), do: true
|
defp extra_diagnostic?(%TokenMissingError{}), do: true
|
||||||
defp code_error?(%CompileError{}), do: true
|
|
||||||
defp code_error?(_error), do: false
|
defp extra_diagnostic?(%CompileError{
|
||||||
|
description: "cannot compile file (errors have been logged)"
|
||||||
|
}),
|
||||||
|
do: false
|
||||||
|
|
||||||
|
defp extra_diagnostic?(%CompileError{}), do: true
|
||||||
|
defp extra_diagnostic?(_error), do: false
|
||||||
|
|
||||||
defp identifier_dependencies(context, tracer_info, prev_context) do
|
defp identifier_dependencies(context, tracer_info, prev_context) do
|
||||||
identifiers_used = MapSet.new()
|
identifiers_used = MapSet.new()
|
||||||
|
|
|
@ -1806,7 +1806,7 @@ defmodule LivebookWeb.SessionLive do
|
||||||
) do
|
) do
|
||||||
socket
|
socket
|
||||||
|> prune_outputs()
|
|> prune_outputs()
|
||||||
|> push_event("evaluation_finished:#{cell_id}", %{code_error: metadata.code_error})
|
|> push_event("evaluation_finished:#{cell_id}", %{code_markers: metadata.code_markers})
|
||||||
end
|
end
|
||||||
|
|
||||||
defp after_operation(
|
defp after_operation(
|
||||||
|
|
BIN
main.zip
Normal file
BIN
main.zip
Normal file
Binary file not shown.
|
@ -21,16 +21,19 @@ defmodule Livebook.IntellisenseTest do
|
||||||
|
|
||||||
describe "format_code/1" do
|
describe "format_code/1" do
|
||||||
test "formats valid code" do
|
test "formats valid code" do
|
||||||
assert %{code: "1 + 1", code_error: nil} = Intellisense.format_code("1+1")
|
assert %{code: "1 + 1", code_markers: []} = Intellisense.format_code("1+1")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns a syntax error when invalid code is given" do
|
test "returns a syntax error when invalid code is given" do
|
||||||
assert %{
|
assert %{
|
||||||
code: nil,
|
code: nil,
|
||||||
code_error: %{
|
code_markers: [
|
||||||
|
%{
|
||||||
line: 1,
|
line: 1,
|
||||||
description: "syntax error: expression is incomplete"
|
description: "syntax error: expression is incomplete",
|
||||||
|
severity: :error
|
||||||
}
|
}
|
||||||
|
]
|
||||||
} = Intellisense.format_code("1+")
|
} = Intellisense.format_code("1+")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1061,10 +1064,7 @@ defmodule Livebook.IntellisenseTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "completion for struct keys inside struct removes filled keys" do
|
test "completion for struct keys inside struct removes filled keys" do
|
||||||
context =
|
context = eval(do: nil)
|
||||||
eval do
|
|
||||||
struct = %Livebook.IntellisenseTest.MyStruct{}
|
|
||||||
end
|
|
||||||
|
|
||||||
assert [] =
|
assert [] =
|
||||||
Intellisense.get_completion_items(
|
Intellisense.get_completion_items(
|
||||||
|
@ -1081,6 +1081,73 @@ defmodule Livebook.IntellisenseTest do
|
||||||
refute Enum.find(completions, &match?(%{label: "__exception__"}, &1))
|
refute Enum.find(completions, &match?(%{label: "__exception__"}, &1))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "completion for struct keys in update syntax" do
|
||||||
|
context = eval(do: nil)
|
||||||
|
|
||||||
|
assert [
|
||||||
|
%{
|
||||||
|
label: "my_val",
|
||||||
|
kind: :field,
|
||||||
|
detail: "Livebook.IntellisenseTest.MyStruct struct field",
|
||||||
|
documentation: _my_val_doc,
|
||||||
|
insert_text: "my_val: "
|
||||||
|
}
|
||||||
|
] =
|
||||||
|
Intellisense.get_completion_items(
|
||||||
|
"%Livebook.IntellisenseTest.MyStruct{struct | ",
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
assert [
|
||||||
|
%{
|
||||||
|
label: "my_val",
|
||||||
|
kind: :field,
|
||||||
|
detail: "Livebook.IntellisenseTest.MyStruct struct field",
|
||||||
|
documentation: _my_val_doc,
|
||||||
|
insert_text: "my_val: "
|
||||||
|
}
|
||||||
|
] =
|
||||||
|
Intellisense.get_completion_items(
|
||||||
|
"%Livebook.IntellisenseTest.MyStruct{struct | my_v",
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
assert [] =
|
||||||
|
Intellisense.get_completion_items(
|
||||||
|
"%Livebook.IntellisenseTest.MyStruct{struct | my_val: 123, ",
|
||||||
|
context
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "completion for map keys in update syntax" do
|
||||||
|
context =
|
||||||
|
eval do
|
||||||
|
map = %{foo: 1}
|
||||||
|
end
|
||||||
|
|
||||||
|
assert [
|
||||||
|
%{
|
||||||
|
label: "foo",
|
||||||
|
kind: :field,
|
||||||
|
detail: "field",
|
||||||
|
documentation: nil,
|
||||||
|
insert_text: "foo: "
|
||||||
|
}
|
||||||
|
] = Intellisense.get_completion_items("%{map | ", context)
|
||||||
|
|
||||||
|
assert [
|
||||||
|
%{
|
||||||
|
label: "foo",
|
||||||
|
kind: :field,
|
||||||
|
detail: "field",
|
||||||
|
documentation: nil,
|
||||||
|
insert_text: "foo: "
|
||||||
|
}
|
||||||
|
] = Intellisense.get_completion_items("%{map | fo", context)
|
||||||
|
|
||||||
|
assert [] = Intellisense.get_completion_items("%{map | foo: 2, ", context)
|
||||||
|
end
|
||||||
|
|
||||||
test "ignore invalid Elixir module literals" do
|
test "ignore invalid Elixir module literals" do
|
||||||
context = eval(do: nil)
|
context = eval(do: nil)
|
||||||
|
|
||||||
|
@ -1190,6 +1257,31 @@ defmodule Livebook.IntellisenseTest do
|
||||||
label: "integer"
|
label: "integer"
|
||||||
} in Intellisense.get_completion_items("<<a::", context)
|
} in Intellisense.get_completion_items("<<a::", context)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "completion for aliases in special forms" do
|
||||||
|
context = eval(do: nil)
|
||||||
|
|
||||||
|
assert [
|
||||||
|
%{
|
||||||
|
label: "Range",
|
||||||
|
kind: :struct,
|
||||||
|
detail: "struct",
|
||||||
|
documentation: "Returns an inclusive range between dates.",
|
||||||
|
insert_text: "Range"
|
||||||
|
}
|
||||||
|
] = Intellisense.get_completion_items("alias Date.", context)
|
||||||
|
|
||||||
|
assert %{
|
||||||
|
label: "Atom",
|
||||||
|
kind: :module,
|
||||||
|
detail: "module",
|
||||||
|
documentation: "Atoms are constants whose values are their own name.",
|
||||||
|
insert_text: "Atom"
|
||||||
|
} in Intellisense.get_completion_items("alias ", context)
|
||||||
|
|
||||||
|
refute Intellisense.get_completion_items("alias ", context)
|
||||||
|
|> Enum.any?(&(&1.kind == :function))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "get_details/3" do
|
describe "get_details/3" do
|
||||||
|
|
|
@ -29,7 +29,7 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
||||||
%{
|
%{
|
||||||
evaluation_time_ms: _,
|
evaluation_time_ms: _,
|
||||||
memory_usage: %{},
|
memory_usage: %{},
|
||||||
code_error: _,
|
code_markers: _,
|
||||||
identifiers_used: _,
|
identifiers_used: _,
|
||||||
identifiers_defined: _
|
identifiers_defined: _
|
||||||
}
|
}
|
||||||
|
@ -61,10 +61,7 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
||||||
Evaluator.evaluate_code(evaluator, "x", :code_2, [])
|
Evaluator.evaluate_code(evaluator, "x", :code_2, [])
|
||||||
|
|
||||||
assert_receive {:runtime_evaluation_response, :code_2,
|
assert_receive {:runtime_evaluation_response, :code_2,
|
||||||
{:error, _kind,
|
{:error, _kind, %CompileError{}, _stacktrace}, metadata()}
|
||||||
%CompileError{
|
|
||||||
description: "undefined function x/0 (there is no such import)"
|
|
||||||
}, _stacktrace}, metadata()}
|
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -163,12 +160,15 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
||||||
Evaluator.evaluate_code(evaluator, code, :code_1, [], file: "file.ex")
|
Evaluator.evaluate_code(evaluator, code, :code_1, [], file: "file.ex")
|
||||||
|
|
||||||
assert_receive {:runtime_evaluation_response, :code_1,
|
assert_receive {:runtime_evaluation_response, :code_1,
|
||||||
{:error, :error, %TokenMissingError{}, _stacktrace},
|
{:error, :error, %TokenMissingError{}, []},
|
||||||
|
%{
|
||||||
|
code_markers: [
|
||||||
%{
|
%{
|
||||||
code_error: %{
|
|
||||||
line: 1,
|
line: 1,
|
||||||
description: "syntax error: expression is incomplete"
|
description: "syntax error: expression is incomplete",
|
||||||
|
severity: :error
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}}
|
}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -180,10 +180,13 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
||||||
assert_receive {:runtime_evaluation_response, :code_1,
|
assert_receive {:runtime_evaluation_response, :code_1,
|
||||||
{:error, :error, %CompileError{}, _stacktrace},
|
{:error, :error, %CompileError{}, _stacktrace},
|
||||||
%{
|
%{
|
||||||
code_error: %{
|
code_markers: [
|
||||||
|
%{
|
||||||
line: 1,
|
line: 1,
|
||||||
description: "undefined function x/0 (there is no such import)"
|
description: ~s/undefined variable "x"/,
|
||||||
|
severity: :error
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}}
|
}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -195,11 +198,13 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
||||||
Evaluator.evaluate_code(evaluator, code, :code_1, [], file: "file.ex")
|
Evaluator.evaluate_code(evaluator, code, :code_1, [], file: "file.ex")
|
||||||
|
|
||||||
expected_stacktrace = [
|
expected_stacktrace = [
|
||||||
|
{:elixir_expand, :expand, 3, [file: ~c"src/elixir_expand.erl", line: 383]},
|
||||||
{:elixir_eval, :__FILE__, 1, [file: ~c"file.ex", line: 1]}
|
{:elixir_eval, :__FILE__, 1, [file: ~c"file.ex", line: 1]}
|
||||||
]
|
]
|
||||||
|
|
||||||
assert_receive {:runtime_evaluation_response, :code_1,
|
assert_receive {:runtime_evaluation_response, :code_1,
|
||||||
{:error, :error, %CompileError{}, ^expected_stacktrace}, %{code_error: nil}}
|
{:error, :error, %CompileError{}, ^expected_stacktrace},
|
||||||
|
%{code_markers: []}}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "in case of an error returns only the relevant part of stacktrace",
|
test "in case of an error returns only the relevant part of stacktrace",
|
||||||
|
@ -331,11 +336,14 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
||||||
assert_receive {:runtime_evaluation_response, :code_2,
|
assert_receive {:runtime_evaluation_response, :code_2,
|
||||||
{:error, :error, %CompileError{}, []},
|
{:error, :error, %CompileError{}, []},
|
||||||
%{
|
%{
|
||||||
code_error: %{
|
code_markers: [
|
||||||
|
%{
|
||||||
line: 1,
|
line: 1,
|
||||||
description:
|
description:
|
||||||
"module Livebook.Runtime.EvaluatorTest.Redefinition is already defined"
|
"module Livebook.Runtime.EvaluatorTest.Redefinition is already defined",
|
||||||
|
severity: :error
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}}
|
}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -633,12 +641,17 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "reports parentheses-less arity-0 import as a used variable", %{evaluator: evaluator} do
|
test "reports parentheses-less arity-0 import as a used variable", %{evaluator: evaluator} do
|
||||||
|
# TODO: remove all logic around undefined unused vars once we require Elixir v1.15
|
||||||
|
Code.put_compiler_option(:on_undefined_variable, :warn)
|
||||||
|
|
||||||
identifiers =
|
identifiers =
|
||||||
"""
|
"""
|
||||||
self
|
self
|
||||||
"""
|
"""
|
||||||
|> eval(evaluator, 0)
|
|> eval(evaluator, 0)
|
||||||
|
|
||||||
|
Code.put_compiler_option(:on_undefined_variable, :raise)
|
||||||
|
|
||||||
assert {:variable, {:self, nil}} in identifiers.used
|
assert {:variable, {:self, nil}} in identifiers.used
|
||||||
assert :imports in identifiers.used
|
assert :imports in identifiers.used
|
||||||
end
|
end
|
||||||
|
@ -961,10 +974,7 @@ defmodule Livebook.Runtime.EvaluatorTest do
|
||||||
Evaluator.evaluate_code(evaluator, "x", :code_2, [:code_1])
|
Evaluator.evaluate_code(evaluator, "x", :code_2, [:code_1])
|
||||||
|
|
||||||
assert_receive {:runtime_evaluation_response, :code_2,
|
assert_receive {:runtime_evaluation_response, :code_2,
|
||||||
{:error, _kind,
|
{:error, _kind, %CompileError{}, _stacktrace}, metadata()}
|
||||||
%CompileError{
|
|
||||||
description: "undefined function x/0 (there is no such import)"
|
|
||||||
}, _stacktrace}, metadata()}
|
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -718,6 +718,8 @@ defmodule Livebook.SessionTest do
|
||||||
describe "start_link/1" do
|
describe "start_link/1" do
|
||||||
@tag :tmp_dir
|
@tag :tmp_dir
|
||||||
test "fails if the given path is already in use", %{tmp_dir: tmp_dir} do
|
test "fails if the given path is already in use", %{tmp_dir: tmp_dir} do
|
||||||
|
Process.flag(:trap_exit, true)
|
||||||
|
|
||||||
tmp_dir = FileSystem.File.local(tmp_dir <> "/")
|
tmp_dir = FileSystem.File.local(tmp_dir <> "/")
|
||||||
file = FileSystem.File.resolve(tmp_dir, "notebook.livemd")
|
file = FileSystem.File.resolve(tmp_dir, "notebook.livemd")
|
||||||
start_session(file: file)
|
start_session(file: file)
|
||||||
|
|
Loading…
Add table
Reference in a new issue