mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-04 20:14:57 +08:00
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:
parent
38db12fbcc
commit
889503ad68
8 changed files with 44 additions and 282 deletions
6
.github/workflows/assets.yaml
vendored
6
.github/workflows/assets.yaml
vendored
|
@ -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
|
||||
|
|
6
.github/workflows/test.yaml
vendored
6
.github/workflows/test.yaml
vendored
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
2
mix.exs
2
mix.exs
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue