Require Elixir 1.13 (#737)

* Bump required Elixir version to 1.13 and fix TODOs

* Fix tests

* Remove the deprecated URI.parse/1

* Bump Docker base image

* Bump Elixir on CI
This commit is contained in:
Jonatan Kłosko 2021-12-03 21:47:20 +01:00 committed by GitHub
parent ac6b423e79
commit 47d29cb389
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 91 additions and 837 deletions

View file

@ -16,7 +16,7 @@ jobs:
uses: erlef/setup-beam@v1
with:
otp-version: '24.0'
elixir-version: '1.12.3'
elixir-version: '1.13.0'
- name: Cache Mix
uses: actions/cache@v2
with:

View file

@ -15,7 +15,7 @@ jobs:
uses: erlef/setup-beam@v1
with:
otp-version: '24.0'
elixir-version: '1.12.3'
elixir-version: '1.13.0'
- name: Cache Mix
uses: actions/cache@v2
with:

View file

@ -1,6 +1,6 @@
# Stage 1
# Builds the Livebook release
FROM hexpm/elixir:1.12.3-erlang-24.0.6-debian-bullseye-20210902-slim AS build
FROM hexpm/elixir:1.13.0-erlang-24.1.7-debian-bullseye-20210902-slim AS build
RUN apt-get update && apt-get upgrade -y && \
apt-get install --no-install-recommends -y \
@ -37,7 +37,7 @@ RUN mix do compile, release
# We use the same base image, because we need Erlang, Elixir and Mix
# during runtime to spawn the Livebook standalone runtimes.
# Consequently the release doesn't include ERTS as we have it anyway.
FROM hexpm/elixir:1.12.3-erlang-24.0.6-debian-bullseye-20210902-slim
FROM hexpm/elixir:1.13.0-erlang-24.1.7-debian-bullseye-20210902-slim
RUN apt-get update && apt-get upgrade -y && \
apt-get install --no-install-recommends -y \

View file

@ -18,23 +18,17 @@ defmodule Livebook.ContentLoader do
"""
@spec rewrite_url(String.t()) :: String.t()
def rewrite_url(url) do
url
|> URI.parse()
|> do_rewrite_url()
|> URI.to_string()
case URI.new(url) do
{:ok, uri} -> uri |> do_rewrite_url() |> URI.to_string()
{:error, _} -> url
end
end
defp do_rewrite_url(%URI{host: "github.com"} = uri) do
case String.split(uri.path, "/") do
["", owner, repo, "blob", hash | file_path] ->
path = Enum.join(["", owner, repo, hash | file_path], "/")
%{
uri
| path: path,
host: "raw.githubusercontent.com",
authority: "raw.githubusercontent.com"
}
%{uri | path: path, host: "raw.githubusercontent.com"}
_ ->
uri
@ -45,13 +39,7 @@ defmodule Livebook.ContentLoader do
case String.split(uri.path, "/") do
["", owner, hash] ->
path = Enum.join(["", owner, hash, "raw"], "/")
%{
uri
| path: path,
host: "gist.githubusercontent.com",
authority: "gist.githubusercontent.com"
}
%{uri | path: path, host: "gist.githubusercontent.com"}
_ ->
uri

View file

@ -366,7 +366,7 @@ defimpl Livebook.FileSystem, for: Livebook.FileSystem.S3 do
headers = opts[:headers] || []
body = opts[:body]
%{host: host} = URI.parse(file_system.bucket_url)
%{host: host} = URI.new!(file_system.bucket_url)
url = file_system.bucket_url <> path <> "?" <> URI.encode_query(query)
@ -394,7 +394,7 @@ defimpl Livebook.FileSystem, for: Livebook.FileSystem.S3 do
defp region_from_uri(uri) do
# For many services the API host is of the form *.[region].[rootdomain].com
%{host: host} = URI.parse(uri)
%{host: host} = URI.new!(uri)
host |> String.split(".") |> Enum.reverse() |> Enum.at(2, "auto")
end

View file

@ -45,7 +45,7 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
@spec completion_identifiers(String.t(), Code.binding(), Macro.Env.t()) ::
list(identifier_item())
def completion_identifiers(hint, binding, env) do
context = cursor_context(hint)
context = Code.Fragment.cursor_context(hint)
ctx = %{binding: binding, env: env, matcher: @prefix_matcher}
context_to_matches(context, ctx, :completion)
end
@ -63,7 +63,7 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
range: nil | %{from: pos_integer(), to: pos_integer()}
}
def locate_identifier(line, column, binding, env) do
case surround_context(line, {1, column}) do
case Code.Fragment.surround_context(line, {1, column}) do
%{context: context, begin: {_, from}, end: {_, to}} ->
ctx = %{binding: binding, env: env, matcher: @exact_matcher}
matches = context_to_matches(context, ctx, :locate)
@ -514,738 +514,4 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
ctx.matcher.(name, hint),
do: {:module_attribute, name, {"text/markdown", info.doc}}
end
# ---
# Source of Code.Fragment
# TODO: no longer required on Elixir v1.13
# This module provides conveniences for analyzing fragments of
# textual code and extract available information whenever possible.
# Most of the functions in this module provide a best-effort
# and may not be accurate under all circumstances. Read each
# documentation for more information.
# This module should be considered experimental.
@type position :: {line :: pos_integer(), column :: pos_integer()}
@doc """
Receives a string and returns the cursor context.
This function receives a string with an Elixir code fragment,
representing a cursor position, and based on the string, it
provides contextual information about said position. The
return of this function can then be used to provide tips,
suggestions, and autocompletion functionality.
This function provides a best-effort detection and may not be
accurate under all circumstances. See the "Limitations"
section below.
Consider adding a catch-all clause when handling the return
type of this function as new cursor information may be added
in future releases.
## Examples
iex> Code.Fragment.cursor_context("")
:expr
iex> Code.Fragment.cursor_context("hello_wor")
{:local_or_var, 'hello_wor'}
## Return values
* `{:alias, charlist}` - the context is an alias, potentially
a nested one, such as `Hello.Wor` or `HelloWor`
* `{:dot, inside_dot, charlist}` - the context is a dot
where `inside_dot` is either a `{:var, charlist}`, `{:alias, charlist}`,
`{:module_attribute, charlist}`, `{:unquoted_atom, charlist}` or a `dot`
itself. If a var is given, this may either be a remote call or a map
field access. Examples are `Hello.wor`, `:hello.wor`, `hello.wor`,
`Hello.nested.wor`, `hello.nested.wor`, and `@hello.world`
* `{:dot_arity, inside_dot, charlist}` - the context is a dot arity
where `inside_dot` is either a `{:var, charlist}`, `{:alias, charlist}`,
`{:module_attribute, charlist}`, `{:unquoted_atom, charlist}` or a `dot`
itself. If a var is given, it must be a remote arity. Examples are
`Hello.world/`, `:hello.world/`, `hello.world/2`, and `@hello.world/2`
* `{:dot_call, inside_dot, charlist}` - the context is a dot
call. This means parentheses or space have been added after the expression.
where `inside_dot` is either a `{:var, charlist}`, `{:alias, charlist}`,
`{:module_attribute, charlist}`, `{:unquoted_atom, charlist}` or a `dot`
itself. If a var is given, it must be a remote call. Examples are
`Hello.world(`, `:hello.world(`, `Hello.world `, `hello.world(`, `hello.world `,
and `@hello.world(`
* `:expr` - may be any expression. Autocompletion may suggest an alias,
local or var
* `{:local_or_var, charlist}` - the context is a variable or a local
(import or local) call, such as `hello_wor`
* `{:local_arity, charlist}` - the context is a local (import or local)
arity, such as `hello_world/`
* `{:local_call, charlist}` - the context is a local (import or local)
call, such as `hello_world(` and `hello_world `
* `{:module_attribute, charlist}` - the context is a module attribute,
such as `@hello_wor`
* `{:operator, charlist}` - the context is an operator, such as `+` or
`==`. Note textual operators, such as `when` do not appear as operators
but rather as `:local_or_var`. `@` is never an `:operator` and always a
`:module_attribute`
* `{:operator_arity, charlist}` - the context is an operator arity, which
is an operator followed by /, such as `+/`, `not/` or `when/`
* `{:operator_call, charlist}` - the context is an operator call, which is
an operator followed by space, such as `left + `, `not ` or `x when `
* `:none` - no context possible
* `{:sigil, charlist}` - the context is a sigil. It may be either the beginning
of a sigil, such as `~` or `~s`, or an operator starting with `~`, such as
`~>` and `~>>`
* `{:struct, charlist}` - the context is a struct, such as `%`, `%UR` or `%URI`
* `{:unquoted_atom, charlist}` - the context is an unquoted atom. This
can be any atom or an atom representing a module
## Limitations
The current algorithm only considers the last line of the input. This means
it will also show suggestions inside strings, heredocs, etc, which is
intentional as it helps with doctests, references, and more. Other functions
may be added in the future that consider the tree-structure of the code.
"""
@doc since: "1.13.0"
@spec cursor_context(List.Chars.t(), keyword()) ::
{:alias, charlist}
| {:dot, inside_dot, charlist}
| {:dot_arity, inside_dot, charlist}
| {:dot_call, inside_dot, charlist}
| :expr
| {:local_or_var, charlist}
| {:local_arity, charlist}
| {:local_call, charlist}
| {:module_attribute, charlist}
| {:operator, charlist}
| {:operator_arity, charlist}
| {:operator_call, charlist}
| :none
| {:sigil, charlist}
| {:struct, charlist}
| {:unquoted_atom, charlist}
when inside_dot:
{:alias, charlist}
| {:dot, inside_dot, charlist}
| {:module_attribute, charlist}
| {:unquoted_atom, charlist}
| {:var, charlist}
def cursor_context(fragment, opts \\ [])
def cursor_context(binary, opts) when is_binary(binary) and is_list(opts) do
binary =
case :binary.matches(binary, "\n") do
[] ->
binary
matches ->
{position, _} = List.last(matches)
binary_part(binary, position + 1, byte_size(binary) - position - 1)
end
binary
|> String.to_charlist()
|> :lists.reverse()
|> codepoint_cursor_context(opts)
|> elem(0)
end
def cursor_context(charlist, opts) when is_list(charlist) and is_list(opts) do
charlist =
case charlist |> Enum.chunk_by(&(&1 == ?\n)) |> List.last([]) do
[?\n | _] -> []
rest -> rest
end
charlist
|> :lists.reverse()
|> codepoint_cursor_context(opts)
|> elem(0)
end
def cursor_context(other, opts) when is_list(opts) do
cursor_context(to_charlist(other), opts)
end
@operators '\\<>+-*/:=|&~^%!'
@starter_punctuation ',([{;'
@non_starter_punctuation ')]}"\'.$'
@space '\t\s'
@trailing_identifier '?!'
@tilde_op_prefix '<=~'
@non_identifier @trailing_identifier ++
@operators ++ @starter_punctuation ++ @non_starter_punctuation ++ @space
@textual_operators ~w(when not and or in)c
@incomplete_operators ~w(^^ ~~ ~)c
defp codepoint_cursor_context(reverse, _opts) do
{stripped, spaces} = strip_spaces(reverse, 0)
case stripped do
# It is empty
[] -> {:expr, 0}
# Structs
[?%, ?:, ?: | _] -> {{:struct, ''}, 1}
[?%, ?: | _] -> {{:unquoted_atom, '%'}, 2}
[?% | _] -> {{:struct, ''}, 1}
# Token/AST only operators
[?>, ?= | rest] when rest == [] or hd(rest) != ?: -> {:expr, 0}
[?>, ?- | rest] when rest == [] or hd(rest) != ?: -> {:expr, 0}
# Two-digit containers
[?<, ?< | rest] when rest == [] or hd(rest) != ?< -> {:expr, 0}
# Ambiguity around :
[?: | rest] when rest == [] or hd(rest) != ?: -> unquoted_atom_or_expr(spaces)
# Dots
[?.] -> {:none, 0}
[?. | rest] when hd(rest) not in '.:' -> dot(rest, spaces + 1, '')
# It is a local or remote call with parens
[?( | rest] -> call_to_cursor_context(strip_spaces(rest, spaces + 1))
# A local arity definition
[?/ | rest] -> arity_to_cursor_context(strip_spaces(rest, spaces + 1))
# Starting a new expression
[h | _] when h in @starter_punctuation -> {:expr, 0}
# It is a local or remote call without parens
rest when spaces > 0 -> call_to_cursor_context({rest, spaces})
# It is an identifier
_ -> identifier_to_cursor_context(reverse, 0, false)
end
end
defp strip_spaces([h | rest], count) when h in @space, do: strip_spaces(rest, count + 1)
defp strip_spaces(rest, count), do: {rest, count}
defp unquoted_atom_or_expr(0), do: {{:unquoted_atom, ''}, 1}
defp unquoted_atom_or_expr(_), do: {:expr, 0}
defp arity_to_cursor_context({reverse, spaces}) do
case identifier_to_cursor_context(reverse, spaces, true) do
{{:local_or_var, acc}, count} -> {{:local_arity, acc}, count}
{{:dot, base, acc}, count} -> {{:dot_arity, base, acc}, count}
{{:operator, acc}, count} -> {{:operator_arity, acc}, count}
{_, _} -> {:none, 0}
end
end
defp call_to_cursor_context({reverse, spaces}) do
case identifier_to_cursor_context(reverse, spaces, true) do
{{:local_or_var, acc}, count} -> {{:local_call, acc}, count}
{{:dot, base, acc}, count} -> {{:dot_call, base, acc}, count}
{{:operator, acc}, count} -> {{:operator_call, acc}, count}
{_, _} -> {:none, 0}
end
end
defp identifier_to_cursor_context([?., ?., ?: | _], n, _), do: {{:unquoted_atom, '..'}, n + 3}
defp identifier_to_cursor_context([?., ?., ?. | _], n, _), do: {{:local_or_var, '...'}, n + 3}
defp identifier_to_cursor_context([?., ?: | _], n, _), do: {{:unquoted_atom, '.'}, n + 2}
defp identifier_to_cursor_context([?., ?. | _], n, _), do: {{:operator, '..'}, n + 2}
defp identifier_to_cursor_context(reverse, count, call_op?) do
case identifier(reverse, count) do
:none ->
{:none, 0}
:operator ->
operator(reverse, count, [], call_op?)
{:module_attribute, acc, count} ->
{{:module_attribute, acc}, count}
{:sigil, acc, count} ->
{{:sigil, acc}, count}
{:unquoted_atom, acc, count} ->
{{:unquoted_atom, acc}, count}
{:alias, rest, acc, count} ->
case strip_spaces(rest, count) do
{'.' ++ rest, count} when rest == [] or hd(rest) != ?. ->
nested_alias(rest, count + 1, acc)
{'%' ++ _, count} ->
{{:struct, acc}, count + 1}
_ ->
{{:alias, acc}, count}
end
{:identifier, _, acc, count} when call_op? and acc in @textual_operators ->
{{:operator, acc}, count}
{:identifier, rest, acc, count} ->
case strip_spaces(rest, count) do
{'.' ++ rest, count} when rest == [] or hd(rest) != ?. ->
dot(rest, count + 1, acc)
_ ->
{{:local_or_var, acc}, count}
end
end
end
defp identifier([?? | rest], count), do: check_identifier(rest, count + 1, [??])
defp identifier([?! | rest], count), do: check_identifier(rest, count + 1, [?!])
defp identifier(rest, count), do: check_identifier(rest, count, [])
defp check_identifier([h | t], count, acc) when h not in @non_identifier,
do: rest_identifier(t, count + 1, [h | acc])
defp check_identifier(_, _, _), do: :operator
defp rest_identifier([h | rest], count, acc) when h not in @non_identifier do
rest_identifier(rest, count + 1, [h | acc])
end
defp rest_identifier(rest, count, [?@ | acc]) do
case tokenize_identifier(rest, count, acc) do
{:identifier, _rest, acc, count} -> {:module_attribute, acc, count}
:none when acc == [] -> {:module_attribute, '', count}
_ -> :none
end
end
defp rest_identifier([?~ | rest], count, [letter])
when (letter in ?A..?Z or letter in ?a..?z) and
(rest == [] or hd(rest) not in @tilde_op_prefix) do
{:sigil, [letter], count + 1}
end
defp rest_identifier([?: | rest], count, acc) when rest == [] or hd(rest) != ?: do
case String.Tokenizer.tokenize(acc) do
{_, _, [], _, _, _} -> {:unquoted_atom, acc, count + 1}
_ -> :none
end
end
defp rest_identifier([?? | _], _count, _acc) do
:none
end
defp rest_identifier(rest, count, acc) do
tokenize_identifier(rest, count, acc)
end
defp tokenize_identifier(rest, count, acc) do
case String.Tokenizer.tokenize(acc) do
# Not actually an atom cause rest is not a :
{:atom, _, _, _, _, _} ->
:none
# Aliases must be ascii only
{:alias, _, _, _, false, _} ->
:none
{kind, _, [], _, _, extra} ->
if ?@ in extra do
:none
else
{kind, rest, acc, count}
end
_ ->
:none
end
end
defp nested_alias(rest, count, acc) do
{rest, count} = strip_spaces(rest, count)
case identifier_to_cursor_context(rest, count, true) do
{{:struct, prev}, count} -> {{:struct, prev ++ '.' ++ acc}, count}
{{:alias, prev}, count} -> {{:alias, prev ++ '.' ++ acc}, count}
_ -> {:none, 0}
end
end
defp dot(rest, count, acc) do
{rest, count} = strip_spaces(rest, count)
case identifier_to_cursor_context(rest, count, true) do
{{:local_or_var, var}, count} -> {{:dot, {:var, var}, acc}, count}
{{:unquoted_atom, _} = prev, count} -> {{:dot, prev, acc}, count}
{{:alias, _} = prev, count} -> {{:dot, prev, acc}, count}
{{:dot, _, _} = prev, count} -> {{:dot, prev, acc}, count}
{{:module_attribute, _} = prev, count} -> {{:dot, prev, acc}, count}
{{:struct, acc}, count} -> {{:struct, acc ++ '.'}, count}
{_, _} -> {:none, 0}
end
end
defp operator([h | rest], count, acc, call_op?) when h in @operators do
operator(rest, count + 1, [h | acc], call_op?)
end
defp operator(rest, count, acc, call_op?) when acc in @incomplete_operators do
{rest, dot_count} = strip_spaces(rest, count)
cond do
call_op? ->
{:none, 0}
match?([?. | rest] when rest == [] or hd(rest) != ?., rest) ->
dot(tl(rest), dot_count + 1, acc)
acc == '~' ->
{{:sigil, ''}, count}
true ->
{{:operator, acc}, count}
end
end
# If we are opening a sigil, ignore the operator.
defp operator([letter, ?~ | rest], _count, [op], _call_op?)
when op in '<|/' and (letter in ?A..?Z or letter in ?a..?z) and
(rest == [] or hd(rest) not in @tilde_op_prefix) do
{:none, 0}
end
defp operator(rest, count, acc, _call_op?) do
case elixir_tokenizer_tokenize(acc, 1, 1, []) do
{:ok, _, [{:atom, _, _}]} ->
{{:unquoted_atom, tl(acc)}, count}
{:ok, _, [{_, _, op}]} ->
{rest, dot_count} = strip_spaces(rest, count)
cond do
Code.Identifier.unary_op(op) == :error and Code.Identifier.binary_op(op) == :error ->
:none
match?([?. | rest] when rest == [] or hd(rest) != ?., rest) ->
dot(tl(rest), dot_count + 1, acc)
true ->
{{:operator, acc}, count}
end
_ ->
{:none, 0}
end
end
@doc """
Receives a string and returns the surround context.
This function receives a string with an Elixir code fragment
and a `position`. It returns a map containing the beginning
and ending of the expression alongside its context, or `:none`
if there is nothing with a known context.
The difference between `cursor_context/2` and `surround_context/3`
is that the former assumes the expression in the code fragment
is incomplete. For example, `do` in `cursor_context/2` may be
a keyword or a variable or a local call, while `surround_context/3`
assumes the expression in the code fragment is complete, therefore
`do` would always be a keyword.
The `position` contains both the `line` and `column`, both starting
with the index of 1. The column must precede the surrounding expression.
For example, the expression `foo`, will return something for the columns
1, 2, and 3, but not 4:
foo
^ column 1
foo
^ column 2
foo
^ column 3
foo
^ column 4
The returned map contains the column the expression starts and the
first column after the expression ends.
Similar to `cursor_context/2`, this function also provides a best-effort
detection and may not be accurate under all circumstances. See the
"Return values" and "Limitations" section under `cursor_context/2` for
more information.
## Examples
iex> Code.Fragment.surround_context("foo", {1, 1})
%{begin: {1, 1}, context: {:local_or_var, 'foo'}, end: {1, 4}}
## Differences to `cursor_context/2`
Because `surround_context/3` deals with complete code, it has some
difference to `cursor_context/2`:
* `dot_call`/`dot_arity` and `operator_call`/`operator_arity`
are collapsed into `dot` and `operator` contexts respectively
as they are not meaningful distinction between them
* On the other hand, this function still makes a distinction between
`local_call`/`local_arity` and `local_or_var`, since the latter can
be a local or variable
* `@` when not followed by any identifier is returned as `{:operator, '@'}`
(in contrast to `{:module_attribute, ''}` in `cursor_context/2`
* This function never returns empty sigils `{:sigil, ''}` or empty structs
`{:struct, ''}` as context
"""
@doc since: "1.13.0"
@spec surround_context(List.Chars.t(), position(), keyword()) ::
%{begin: position, end: position, context: context} | :none
when context:
{:alias, charlist}
| {:dot, inside_dot, charlist}
| {:local_or_var, charlist}
| {:local_arity, charlist}
| {:local_call, charlist}
| {:module_attribute, charlist}
| {:operator, charlist}
| {:unquoted_atom, charlist},
inside_dot:
{:alias, charlist}
| {:dot, inside_dot, charlist}
| {:module_attribute, charlist}
| {:unquoted_atom, charlist}
| {:var, charlist}
def surround_context(fragment, position, options \\ [])
def surround_context(binary, {line, column}, opts) when is_binary(binary) do
binary
|> String.split("\n")
|> Enum.at(line - 1, '')
|> String.to_charlist()
|> position_surround_context(line, column, opts)
end
def surround_context(charlist, {line, column}, opts) when is_list(charlist) do
charlist
|> :string.split('\n', :all)
|> Enum.at(line - 1, '')
|> position_surround_context(line, column, opts)
end
def surround_context(other, position, opts) do
surround_context(to_charlist(other), position, opts)
end
defp position_surround_context(charlist, line, column, opts)
when is_integer(line) and line >= 1 and is_integer(column) and column >= 1 do
{reversed_pre, post} = string_reverse_at(charlist, column - 1, [])
{reversed_pre, post} = adjust_position(reversed_pre, post)
case take_identifier(post, []) do
{_, [], _} ->
maybe_operator(reversed_pre, post, line, opts)
{:identifier, reversed_post, rest} ->
{rest, _} = strip_spaces(rest, 0)
reversed = reversed_post ++ reversed_pre
case codepoint_cursor_context(reversed, opts) do
{{:struct, acc}, offset} ->
build_surround({:struct, acc}, reversed, line, offset)
{{:alias, acc}, offset} ->
build_surround({:alias, acc}, reversed, line, offset)
{{:dot, _, [_ | _]} = dot, offset} ->
build_surround(dot, reversed, line, offset)
{{:local_or_var, acc}, offset} when hd(rest) == ?( ->
build_surround({:local_call, acc}, reversed, line, offset)
{{:local_or_var, acc}, offset} when hd(rest) == ?/ ->
build_surround({:local_arity, acc}, reversed, line, offset)
{{:local_or_var, acc}, offset} when acc in @textual_operators ->
build_surround({:operator, acc}, reversed, line, offset)
{{:local_or_var, acc}, offset} when acc not in ~w(do end after else catch rescue)c ->
build_surround({:local_or_var, acc}, reversed, line, offset)
{{:module_attribute, ''}, offset} ->
build_surround({:operator, '@'}, reversed, line, offset)
{{:module_attribute, acc}, offset} ->
build_surround({:module_attribute, acc}, reversed, line, offset)
{{:sigil, acc}, offset} ->
build_surround({:sigil, acc}, reversed, line, offset)
{{:unquoted_atom, acc}, offset} ->
build_surround({:unquoted_atom, acc}, reversed, line, offset)
_ ->
maybe_operator(reversed_pre, post, line, opts)
end
{:alias, reversed_post, _rest} ->
reversed = reversed_post ++ reversed_pre
case codepoint_cursor_context(reversed, opts) do
{{:alias, acc}, offset} ->
build_surround({:alias, acc}, reversed, line, offset)
{{:struct, acc}, offset} ->
build_surround({:struct, acc}, reversed, line, offset)
_ ->
:none
end
end
end
defp maybe_operator(reversed_pre, post, line, opts) do
case take_operator(post, []) do
{[], _rest} ->
:none
{reversed_post, rest} ->
reversed = reversed_post ++ reversed_pre
case codepoint_cursor_context(reversed, opts) do
{{:operator, acc}, offset} when acc not in @incomplete_operators ->
build_surround({:operator, acc}, reversed, line, offset)
{{:sigil, ''}, offset} when hd(rest) in ?A..?Z or hd(rest) in ?a..?z ->
build_surround({:sigil, [hd(rest)]}, [hd(rest) | reversed], line, offset + 1)
{{:dot, _, [_ | _]} = dot, offset} ->
build_surround(dot, reversed, line, offset)
_ ->
:none
end
end
end
defp build_surround(context, reversed, line, offset) do
{post, reversed_pre} = enum_reverse_at(reversed, offset, [])
pre = :lists.reverse(reversed_pre)
pre_length = :string.length(pre) + 1
%{
context: context,
begin: {line, pre_length},
end: {line, pre_length + :string.length(post)}
}
end
defp take_identifier([h | t], acc) when h in @trailing_identifier,
do: {:identifier, [h | acc], t}
defp take_identifier([h | t], acc) when h not in @non_identifier,
do: take_identifier(t, [h | acc])
defp take_identifier(rest, acc) do
with {[?. | t], _} <- strip_spaces(rest, 0),
{[h | _], _} when h in ?A..?Z <- strip_spaces(t, 0) do
take_alias(rest, acc)
else
_ -> {:identifier, acc, rest}
end
end
defp take_alias([h | t], acc) when h not in @non_identifier,
do: take_alias(t, [h | acc])
defp take_alias(rest, acc) do
with {[?. | t], acc} <- move_spaces(rest, acc),
{[h | t], acc} when h in ?A..?Z <- move_spaces(t, [?. | acc]) do
take_alias(t, [h | acc])
else
_ -> {:alias, acc, rest}
end
end
defp take_operator([h | t], acc) when h in @operators, do: take_operator(t, [h | acc])
defp take_operator([h | t], acc) when h == ?., do: take_operator(t, [h | acc])
defp take_operator(rest, acc), do: {acc, rest}
# Unquoted atom handling
defp adjust_position(reversed_pre, [?: | post])
when hd(post) != ?: and (reversed_pre == [] or hd(reversed_pre) != ?:) do
{[?: | reversed_pre], post}
end
defp adjust_position(reversed_pre, [?% | post]) do
adjust_position([?% | reversed_pre], post)
end
# Dot/struct handling
defp adjust_position(reversed_pre, post) do
case move_spaces(post, reversed_pre) do
# If we are between spaces and a dot, move past the dot
{[?. | post], reversed_pre} when hd(post) != ?. and hd(reversed_pre) != ?. ->
{post, reversed_pre} = move_spaces(post, [?. | reversed_pre])
{reversed_pre, post}
_ ->
case strip_spaces(reversed_pre, 0) do
# If there is a dot to our left, make sure to move to the first character
{[?. | rest], _} when rest == [] or hd(rest) not in '.:' ->
{post, reversed_pre} = move_spaces(post, reversed_pre)
{reversed_pre, post}
# If there is a % to our left, make sure to move to the first character
{[?% | _], _} ->
case move_spaces(post, reversed_pre) do
{[h | _] = post, reversed_pre} when h in ?A..?Z ->
{reversed_pre, post}
_ ->
{reversed_pre, post}
end
_ ->
{reversed_pre, post}
end
end
end
defp move_spaces([h | t], acc) when h in @space, do: move_spaces(t, [h | acc])
defp move_spaces(t, acc), do: {t, acc}
defp string_reverse_at(charlist, 0, acc), do: {acc, charlist}
defp string_reverse_at(charlist, n, acc) do
case :unicode_util.gc(charlist) do
[gc | cont] when is_integer(gc) -> string_reverse_at(cont, n - 1, [gc | acc])
[gc | cont] when is_list(gc) -> string_reverse_at(cont, n - 1, :lists.reverse(gc, acc))
[] -> {acc, []}
end
end
defp enum_reverse_at([h | t], n, acc) when n > 0, do: enum_reverse_at(t, n - 1, [h | acc])
defp enum_reverse_at(rest, _, acc), do: {acc, rest}
# ---
# See: https://github.com/elixir-lang/elixir/pull/11143/files#r676519050
def elixir_tokenizer_tokenize(string, line, columns, opts) do
with {:ok, tokens} <- :elixir_tokenizer.tokenize(string, line, columns, opts) do
{:ok, [], tokens}
end
end
end

View file

@ -167,8 +167,10 @@ defmodule Livebook.Utils do
"""
@spec valid_url?(String.t()) :: boolean()
def valid_url?(url) do
uri = URI.parse(url)
uri.scheme != nil and uri.host != nil
case URI.new(url) do
{:ok, uri} -> uri.scheme != nil and uri.host != nil
_ -> false
end
end
@doc """
@ -245,7 +247,7 @@ defmodule Livebook.Utils do
@spec expand_url(String.t(), String.t()) :: String.t()
def expand_url(url, relative_path) do
url
|> URI.parse()
|> URI.new!()
|> Map.update!(:path, fn path ->
path |> Path.dirname() |> Path.join(relative_path) |> Path.expand()
end)

View file

@ -217,9 +217,8 @@ defmodule LivebookCLI.Server do
defp append_path(url, path) do
url
|> URI.parse()
# TODO: remove `&1 || ""` when we require Elixir 1.13
|> Map.update!(:path, &((&1 || "") <> path))
|> URI.new!()
|> Map.update!(:path, &(&1 <> path))
|> URI.to_string()
end

View file

@ -8,7 +8,7 @@ defmodule Livebook.MixProject do
[
app: :livebook,
version: @version,
elixir: "~> 1.12",
elixir: "~> 1.13",
name: "Livebook",
description: @description,
elixirc_paths: elixirc_paths(Mix.env()),
@ -87,12 +87,7 @@ defmodule Livebook.MixProject do
[
"dev.setup": ["deps.get", "cmd npm install --prefix assets"],
"dev.build": ["cmd npm run deploy --prefix ./assets"],
"format.all": ["format", "cmd npm run format --prefix ./assets"],
# TODO: loadconfig no longer required on Elixir v1.13
# Currently this ensures we load configuration before
# compiling dependencies as part of `mix escript.install`.
# See https://github.com/elixir-lang/elixir/commit/a6eefb244b3a5892895a97b2dad4cce2b3c3c5ed
"escript.build": ["loadconfig", "escript.build"]
"format.all": ["format", "cmd npm run format --prefix ./assets"]
]
end

View file

@ -30,8 +30,10 @@ defmodule Livebook.EvaluatorTest do
Evaluator.evaluate_code(evaluator, self(), "x", :code_2)
assert_receive {:evaluation_response, :code_2,
{:error, _kind, %CompileError{description: "undefined function x/0"},
_stacktrace}, %{evaluation_time_ms: _time_ms}}
{:error, _kind,
%CompileError{
description: "undefined function x/0 (there is no such import)"
}, _stacktrace}, %{evaluation_time_ms: _time_ms}}
end)
end
@ -212,8 +214,10 @@ defmodule Livebook.EvaluatorTest do
Evaluator.evaluate_code(evaluator, self(), "x", :code_2, :code_1)
assert_receive {:evaluation_response, :code_2,
{:error, _kind, %CompileError{description: "undefined function x/0"},
_stacktrace}, %{evaluation_time_ms: _time_ms}}
{:error, _kind,
%CompileError{
description: "undefined function x/0 (there is no such import)"
}, _stacktrace}, %{evaluation_time_ms: _time_ms}}
end)
end

View file

@ -514,68 +514,66 @@ defmodule Livebook.IntellisenseTest do
] = Intellisense.get_completion_items("mod.ve", binding, env)
end
# TODO: Enable on Elixir 1.13
test "operator completion" do
{binding, env} = eval(do: nil)
# test "operator completion" do
# {binding, env} = eval(do: nil)
assert [
%{
label: "++/2",
kind: :function,
detail: "left ++ right",
documentation: """
List concatenation operator. Concatenates a proper list and a term, returning a list.
# assert [
# %{
# label: "++/2",
# kind: :function,
# detail: "left ++ right",
# documentation: """
# List concatenation operator. Concatenates a proper list and a term, returning a list.
```
@spec list() ++ term() ::
maybe_improper_list()
```\
""",
insert_text: "++"
},
%{
label: "+/1",
kind: :function,
detail: "+value",
documentation: """
Arithmetic positive unary operator.
# ```
# @spec list() ++ term() ::
# maybe_improper_list()
# ```\
# """,
# insert_text: "++"
# },
# %{
# label: "+/1",
# kind: :function,
# detail: "+value",
# documentation: """
# Arithmetic positive unary operator.
```
@spec +integer() :: integer()
@spec +float() :: float()
```\
""",
insert_text: "+"
},
%{
label: "+/2",
kind: :function,
detail: "left + right",
documentation: """
Arithmetic addition operator.
# ```
# @spec +integer() :: integer()
# @spec +float() :: float()
# ```\
# """,
# insert_text: "+"
# },
# %{
# label: "+/2",
# kind: :function,
# detail: "left + right",
# documentation: """
# Arithmetic addition operator.
```
@spec integer() + integer() ::
integer()
@spec float() + float() :: float()
@spec integer() + float() :: float()
@spec float() + integer() :: float()
```\
""",
insert_text: "+"
}
] = Intellisense.get_completion_items("+", binding, env)
# ```
# @spec integer() + integer() ::
# integer()
# @spec float() + float() :: float()
# @spec integer() + float() :: float()
# @spec float() + integer() :: float()
# ```\
# """,
# insert_text: "+"
# }
# ] = Intellisense.get_completion_items("+", binding, env)
assert [
%{label: "+/1"},
%{label: "+/2"}
] = Intellisense.get_completion_items("+/", binding, env)
# assert [
# %{label: "+/1"},
# %{label: "+/2"}
# ] = Intellisense.get_completion_items("+/", binding, env)
# assert [
# %{label: "++/2"}
# ] = Intellisense.get_completion_items("++/", binding, env)
# end
assert [
%{label: "++/2"}
] = Intellisense.get_completion_items("++/", binding, env)
end
test "map atom key completion" do
{binding, env} =

View file

@ -668,8 +668,9 @@ defmodule LivebookWeb.SessionLiveTest do
test "if the remote notebook is already imported, redirects to the session",
%{conn: conn, test: test} do
index_url = "http://example.com/#{test}/index.livemd"
notebook_url = "http://example.com/#{test}/notebook.livemd"
test_path = test |> to_string() |> URI.encode_www_form()
index_url = "http://example.com/#{test_path}/index.livemd"
notebook_url = "http://example.com/#{test_path}/notebook.livemd"
{:ok, index_session} = Sessions.create_session(origin: {:url, index_url})
{:ok, notebook_session} = Sessions.create_session(origin: {:url, notebook_url})
@ -682,8 +683,9 @@ defmodule LivebookWeb.SessionLiveTest do
test "renders an error message if there are already multiple session imported from the relative URL",
%{conn: conn, test: test} do
index_url = "http://example.com/#{test}/index.livemd"
notebook_url = "http://example.com/#{test}/notebook.livemd"
test_path = test |> to_string() |> URI.encode_www_form()
index_url = "http://example.com/#{test_path}/index.livemd"
notebook_url = "http://example.com/#{test_path}/notebook.livemd"
{:ok, index_session} = Sessions.create_session(origin: {:url, index_url})
{:ok, _notebook_session1} = Sessions.create_session(origin: {:url, notebook_url})