Require Elixir v1.12 (#278)

* Require Elixir v1.12

* Update introductory notebook

* Update base Docker images

* Add completion of reserved module attributes

* Move complete_module_attribute/1
This commit is contained in:
Jonatan Kłosko 2021-05-19 16:46:33 +02:00 committed by GitHub
parent 38db12fbcc
commit 889503ad68
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 44 additions and 282 deletions

View file

@ -9,10 +9,10 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Install Erlang & Elixir
uses: erlef/setup-elixir@v1
uses: erlef/setup-beam@v1
with:
otp-version: '23.2'
elixir-version: '1.11.3'
otp-version: '24.0'
elixir-version: '1.12.0'
# Note: we need to get Phoenix and LV because package.json points to them directly
- name: Install mix dependencies
run: mix deps.get

View file

@ -10,10 +10,10 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Install Erlang & Elixir
uses: erlef/setup-elixir@v1
uses: erlef/setup-beam@v1
with:
otp-version: '23.2'
elixir-version: '1.11.3'
otp-version: '24.0'
elixir-version: '1.12.0'
- name: Install mix dependencies
run: mix deps.get
- name: Check formatting

View file

@ -1,6 +1,6 @@
# Stage 1
# Builds the Livebook release
FROM hexpm/elixir:1.12.0-rc.1-erlang-24.0-rc3-alpine-3.13.3 AS build
FROM hexpm/elixir:1.12.0-erlang-24.0-alpine-3.13.3 AS build
RUN apk add --no-cache build-base git
@ -31,7 +31,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.0-rc.1-erlang-24.0-rc3-alpine-3.13.3
FROM hexpm/elixir:1.12.0-erlang-24.0-alpine-3.13.3
RUN apk add --no-cache \
# Runtime dependencies

View file

@ -40,7 +40,7 @@ mix deps.get --only prod
MIX_ENV=prod mix phx.server
```
You will need [Elixir v1.11](https://elixir-lang.org/install.html) or later.
You will need [Elixir v1.12](https://elixir-lang.org/install.html) or later.
### Escript

View file

@ -39,7 +39,7 @@ defmodule Livebook.Completion do
end
defp complete(hint, ctx) do
case cursor_context(hint) do
case Code.cursor_context(hint) do
{:alias, alias} ->
complete_alias(List.to_string(alias), ctx)
@ -67,7 +67,9 @@ defmodule Livebook.Completion do
{:local_call, _local} ->
complete_default(ctx)
# {:module_attribute, charlist}
{:module_attribute, attribute} ->
complete_module_attribute(List.to_string(attribute))
# :none
_ ->
[]
@ -553,270 +555,17 @@ defmodule Livebook.Completion do
end
end
# TODO: remove this once we require Elixir 1.12
# --------------------------------------------------------------
# This will be available in Elixir 1.12 as Code.cursor_context/2
# See https://github.com/elixir-lang/elixir/pull/10915
# --------------------------------------------------------------
@doc """
Receives a string and returns the cursor context.
This function receives a string with incomplete Elixir code,
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 certain 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.cursor_context("")
:expr
iex> Code.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)
call, 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`
* `:none` - no context possible
* `:unquoted_atom` - the context is an unquoted atom. This can be either
previous atoms or all available `:erlang` modules
## Limitations
* There is no context for operators
* The current algorithm only considers the last line of the input
* Context does not yet track strings, sigils, etc.
* Arguments of functions calls are not currently recognized
"""
@doc since: "1.12.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}
| :none
| {:unquoted_atom, charlist}
when inside_dot:
{:alias, charlist}
| {:dot, inside_dot, charlist}
| {:module_attribute, charlist}
| {:unquoted_atom, charlist}
| {:var, charlist}
def cursor_context(string, 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
do_cursor_context(String.to_charlist(binary), opts)
end
def cursor_context(charlist, opts) when is_list(charlist) and is_list(opts) do
chunked = Enum.chunk_by(charlist, &(&1 == ?\n))
# TODO: Use List.last/2 on Elixir v1.12+
last =
if chunked == [] do
[]
else
List.last(chunked)
end
case last do
[?\n | _] -> do_cursor_context([], opts)
rest -> do_cursor_context(rest, opts)
end
end
def cursor_context(other, opts) do
cursor_context(to_charlist(other), opts)
end
@operators '\\<>+-*/:=|&~^@%'
@non_closing_punctuation '.,([{;'
@closing_punctuation ')]}'
@space '\t\s'
@closing_identifier '?!'
@operators_and_non_closing_puctuation @operators ++ @non_closing_punctuation
@non_identifier @closing_identifier ++
@operators ++ @non_closing_punctuation ++ @closing_punctuation ++ @space
defp do_cursor_context(list, _opts) do
reverse = Enum.reverse(list)
case strip_spaces(reverse, 0) do
# It is empty
{[], _} ->
:expr
{[?: | _], 0} ->
{:unquoted_atom, ''}
{[?@ | _], 0} ->
{:module_attribute, ''}
{[?. | rest], _} ->
dot(rest, '')
# It is a local or remote call with parens
{[?( | rest], _} ->
call_to_cursor_context(rest)
# A local arity definition
{[?/ | rest], _} ->
case identifier_to_cursor_context(rest) do
{:local_or_var, acc} -> {:local_arity, acc}
{:dot, base, acc} -> {:dot_arity, base, acc}
_ -> :none
end
# Starting a new expression
{[h | _], _} when h in @operators_and_non_closing_puctuation ->
:expr
# It is a local or remote call without parens
{rest, spaces} when spaces > 0 ->
call_to_cursor_context(rest)
# It is an identifier
_ ->
identifier_to_cursor_context(reverse)
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 call_to_cursor_context(reverse) do
case identifier_to_cursor_context(reverse) do
{:local_or_var, acc} -> {:local_call, acc}
{:dot, base, acc} -> {:dot_call, base, acc}
_ -> :none
end
end
defp identifier_to_cursor_context(reverse) do
case identifier(reverse) do
# Parse :: first to avoid ambiguity with atoms
{:alias, false, '::' ++ _, _} -> :none
{kind, _, '::' ++ _, acc} -> alias_or_local_or_var(kind, acc)
# Now handle atoms, any other atom is unexpected
{_kind, _, ':' ++ _, acc} -> {:unquoted_atom, acc}
{:atom, _, _, _} -> :none
# Parse .. first to avoid ambiguity with dots
{:alias, false, _, _} -> :none
{kind, _, '..' ++ _, acc} -> alias_or_local_or_var(kind, acc)
# Module attributes
{:alias, _, '@' ++ _, _} -> :none
{:identifier, _, '@' ++ _, acc} -> {:module_attribute, acc}
# Everything else
{:alias, _, '.' ++ rest, acc} -> nested_alias(rest, acc)
{:identifier, _, '.' ++ rest, acc} -> dot(rest, acc)
{kind, _, _, acc} -> alias_or_local_or_var(kind, acc)
:none -> :none
end
end
defp nested_alias(rest, acc) do
case identifier_to_cursor_context(rest) do
{:alias, prev} -> {:alias, prev ++ '.' ++ acc}
_ -> :none
end
end
defp dot(rest, acc) do
case identifier_to_cursor_context(rest) do
{:local_or_var, prev} -> {:dot, {:var, prev}, acc}
{:unquoted_atom, _} = prev -> {:dot, prev, acc}
{:alias, _} = prev -> {:dot, prev, acc}
{:dot, _, _} = prev -> {:dot, prev, acc}
{:module_attribute, _} = prev -> {:dot, prev, acc}
_ -> :none
end
end
defp alias_or_local_or_var(:alias, acc), do: {:alias, acc}
defp alias_or_local_or_var(:identifier, acc), do: {:local_or_var, acc}
defp alias_or_local_or_var(_, _), do: :none
defp identifier([?? | rest]), do: check_identifier(rest, [??])
defp identifier([?! | rest]), do: check_identifier(rest, [?!])
defp identifier(rest), do: check_identifier(rest, [])
defp check_identifier([h | _], _acc) when h in @non_identifier, do: :none
defp check_identifier(rest, acc), do: rest_identifier(rest, acc)
defp rest_identifier([h | rest], acc) when h not in @non_identifier do
rest_identifier(rest, [h | acc])
end
defp rest_identifier(rest, acc) do
case String.Tokenizer.tokenize(acc) do
{kind, _, [], _, ascii_only?, _} -> {kind, ascii_only?, rest, acc}
_ -> :none
defp complete_module_attribute(hint) do
for {attribute, info} <- Module.reserved_attributes(),
name = Atom.to_string(attribute),
String.starts_with?(name, hint) do
%{
label: name,
kind: :variable,
detail: "module attribute",
documentation: info.doc,
insert_text: name
}
end
end
end

View file

@ -140,7 +140,7 @@ defmodule Livebook.Notebook.Welcome do
But there are cases when you just want to play around with a new package
or quickly prototype some code that relies on such. Fortunately, Elixir v1.12+ ships with
[`Mix.install/2`](https://hexdocs.pm/mix/1.12/Mix.html#install/2) that allows you to install
[`Mix.install/2`](https://hexdocs.pm/mix/Mix.html#install/2) that allows you to install
dependencies into your Elixir runtime! This approach is especially useful when sharing notebooks
because everyone will be able to get the same dependencies. Let's try this out:
@ -149,7 +149,6 @@ defmodule Livebook.Notebook.Welcome do
instance, otherwise the command below will fail.
```elixir
# Note: this requires Elixir version >= 1.12
Mix.install([
{:jason, "~> 1.2"}
])
@ -170,16 +169,16 @@ defmodule Livebook.Notebook.Welcome do
## Running tests
If you are using Elixir v1.12, it is also possible to run tests directly
from your notebooks. The key is to disable `ExUnit`'s autorun feature and
then explicitly run the test suite after all test cases have been defined:
It is also possible to run tests directly from your notebooks.
The key is to disable `ExUnit`'s autorun feature and then explicitly
run the test suite after all test cases have been defined:
```elixir
ExUnit.start(autorun: false)
defmodule MyTest do
use ExUnit.Case, async: true
test "it works" do
assert true
end

View file

@ -5,7 +5,7 @@ defmodule Livebook.MixProject do
[
app: :livebook,
version: "0.1.0",
elixir: "~> 1.11",
elixir: "~> 1.12",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:phoenix] ++ Mix.compilers(),
start_permanent: Mix.env() == :prod,

View file

@ -911,4 +911,18 @@ defmodule Livebook.CompletionTest do
:code.purge(:"Elixir.Livebook.CompletionTest.Unicodé")
:code.delete(:"Elixir.Livebook.CompletionTest.Unicodé")
end
test "known Elixir module attributes completion" do
{binding, env} = eval(do: nil)
assert [
%{
label: "moduledoc",
kind: :variable,
detail: "module attribute",
documentation: "Provides documentation for the current module.",
insert_text: "moduledoc"
}
] = Completion.get_completion_items("@modu", binding, env)
end
end