mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-10-26 13:27:05 +08:00
Add completion for struct keys (#793)
* Add completion for struct keys Largely ported from `IEx.Autocomplete`. * Add test for __exception__ field in struct * Fix exception test * Fix exceptions assetion * Create `:in_struct_field` identifier Along with a refactor * Fix typespecs for `:map_field` * Address feedback * Update lib/livebook/intellisense/identifier_matcher.ex Co-authored-by: José Valim <jose.valim@gmail.com> * Use markdown snippet for both docs * Fix tests Co-authored-by: José Valim <jose.valim@gmail.com>
This commit is contained in:
parent
9598fa6b34
commit
4aa5447e9d
3 changed files with 164 additions and 13 deletions
|
|
@ -138,11 +138,30 @@ defmodule Livebook.Intellisense do
|
|||
|
||||
defp format_completion_item({:map_field, name, _value}),
|
||||
do: %{
|
||||
label: name,
|
||||
label: "#{name}",
|
||||
kind: :field,
|
||||
detail: "field",
|
||||
documentation: nil,
|
||||
insert_text: name
|
||||
insert_text: "#{name}"
|
||||
}
|
||||
|
||||
defp format_completion_item({:in_struct_field, struct, name, default}),
|
||||
do: %{
|
||||
label: "#{name}",
|
||||
kind: :field,
|
||||
detail: "#{inspect(struct)} struct field",
|
||||
documentation:
|
||||
join_with_divider([
|
||||
code(name),
|
||||
"""
|
||||
**Default**
|
||||
|
||||
```
|
||||
#{inspect(default, pretty: true, width: @line_length)}
|
||||
```
|
||||
"""
|
||||
]),
|
||||
insert_text: "#{name}: "
|
||||
}
|
||||
|
||||
defp format_completion_item({:module, module, display_name, documentation}) do
|
||||
|
|
@ -340,6 +359,19 @@ defmodule Livebook.Intellisense do
|
|||
|
||||
defp format_details_item({:map_field, name, _value}), do: code(name)
|
||||
|
||||
defp format_details_item({:in_struct_field, _struct, name, default}) do
|
||||
join_with_divider([
|
||||
code(name),
|
||||
"""
|
||||
**Default**
|
||||
|
||||
```
|
||||
#{inspect(default, pretty: true, width: @line_length)}
|
||||
```
|
||||
"""
|
||||
])
|
||||
end
|
||||
|
||||
defp format_details_item({:module, _module, display_name, documentation}) do
|
||||
join_with_divider([
|
||||
code(display_name),
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
|||
@type identifier_item ::
|
||||
{:variable, name(), value()}
|
||||
| {:map_field, name(), value()}
|
||||
| {:in_struct_field, module(), name(), default :: value()}
|
||||
| {:module, module(), display_name(), Docs.documentation()}
|
||||
| {:function, module(), name(), arity(), function_type(), display_name(),
|
||||
Docs.documentation(), list(Docs.signature()), list(Docs.spec())}
|
||||
|
|
@ -48,8 +49,16 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
|||
list(identifier_item())
|
||||
def completion_identifiers(hint, binding, env) do
|
||||
context = Code.Fragment.cursor_context(hint)
|
||||
ctx = %{binding: binding, env: env, matcher: @prefix_matcher}
|
||||
context_to_matches(context, ctx, :completion)
|
||||
|
||||
ctx = %{
|
||||
fragment: hint,
|
||||
binding: binding,
|
||||
env: env,
|
||||
matcher: @prefix_matcher,
|
||||
type: :completion
|
||||
}
|
||||
|
||||
context_to_matches(context, ctx)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
@ -67,8 +76,17 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
|||
def locate_identifier(line, column, binding, env) 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)
|
||||
fragment = String.slice(line, 0, to - 1)
|
||||
|
||||
ctx = %{
|
||||
fragment: fragment,
|
||||
binding: binding,
|
||||
env: env,
|
||||
matcher: @exact_matcher,
|
||||
type: :locate
|
||||
}
|
||||
|
||||
matches = context_to_matches(context, ctx)
|
||||
%{matches: matches, range: %{from: from, to: to}}
|
||||
|
||||
:none ->
|
||||
|
|
@ -79,7 +97,7 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
|||
# Takes a context returned from Code.Fragment.cursor_context
|
||||
# or Code.Fragment.surround_context and looks up matching
|
||||
# identifier items
|
||||
defp context_to_matches(context, ctx, type) do
|
||||
defp context_to_matches(context, ctx) do
|
||||
case context do
|
||||
{:alias, alias} ->
|
||||
match_alias(List.to_string(alias), ctx, false)
|
||||
|
|
@ -100,13 +118,13 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
|||
match_default(ctx)
|
||||
|
||||
{:local_or_var, local_or_var} ->
|
||||
match_local_or_var(List.to_string(local_or_var), ctx)
|
||||
match_in_struct_fields_or_local_or_var(List.to_string(local_or_var), ctx)
|
||||
|
||||
{:local_arity, local} ->
|
||||
match_local(List.to_string(local), %{ctx | matcher: @exact_matcher})
|
||||
|
||||
{:local_call, local} ->
|
||||
case type do
|
||||
case ctx.type do
|
||||
:completion -> match_default(ctx)
|
||||
:locate -> match_local(List.to_string(local), %{ctx | matcher: @exact_matcher})
|
||||
end
|
||||
|
|
@ -178,7 +196,7 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
|||
end
|
||||
|
||||
defp match_default(ctx) do
|
||||
match_local_or_var("", ctx)
|
||||
match_in_struct_fields_or_local_or_var("", ctx)
|
||||
end
|
||||
|
||||
defp match_alias(hint, ctx, nested?) do
|
||||
|
|
@ -195,18 +213,73 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
|||
defp match_struct(hint, ctx) do
|
||||
for {:module, module, name, documentation} <- match_alias(hint, ctx, true),
|
||||
has_struct?(module),
|
||||
not is_exception?(module),
|
||||
do: {:module, module, name, documentation}
|
||||
end
|
||||
|
||||
defp has_struct?(mod) do
|
||||
Code.ensure_loaded?(mod) and function_exported?(mod, :__struct__, 1) and
|
||||
not function_exported?(mod, :exception, 1)
|
||||
Code.ensure_loaded?(mod) and function_exported?(mod, :__struct__, 1)
|
||||
end
|
||||
|
||||
defp is_exception?(mod) do
|
||||
Code.ensure_loaded?(mod) and function_exported?(mod, :exception, 1)
|
||||
end
|
||||
|
||||
defp match_module_member(mod, hint, ctx) do
|
||||
match_module_function(mod, hint, ctx) ++ match_module_type(mod, hint, ctx)
|
||||
end
|
||||
|
||||
defp match_in_struct_fields_or_local_or_var(hint, ctx) do
|
||||
case expand_struct_fields(ctx) do
|
||||
{:ok, struct, fields} ->
|
||||
for {field, default} <- fields,
|
||||
name = Atom.to_string(field),
|
||||
ctx.matcher.(name, hint),
|
||||
do: {:in_struct_field, struct, field, default}
|
||||
|
||||
_ ->
|
||||
match_local_or_var(hint, ctx)
|
||||
end
|
||||
end
|
||||
|
||||
defp expand_struct_fields(ctx) do
|
||||
with {:ok, quoted} <- Code.Fragment.container_cursor_to_quoted(ctx.fragment),
|
||||
{aliases, pairs} <- find_struct_fields(quoted) do
|
||||
mod_name = Enum.join(aliases, ".")
|
||||
mod = expand_alias(mod_name, ctx)
|
||||
|
||||
fields =
|
||||
if has_struct?(mod) do
|
||||
# Remove the keys that have already been filled, and internal keys
|
||||
Map.from_struct(mod.__struct__)
|
||||
|> Map.drop(Keyword.keys(pairs))
|
||||
|> Map.reject(fn {key, _} ->
|
||||
key
|
||||
|> Atom.to_string()
|
||||
|> String.starts_with?("_")
|
||||
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
|
||||
|
||||
defp match_local_or_var(hint, ctx) do
|
||||
match_local(hint, ctx) ++ match_variable(hint, ctx)
|
||||
end
|
||||
|
|
@ -238,7 +311,7 @@ defmodule Livebook.Intellisense.IdentifierMatcher do
|
|||
is_atom(key),
|
||||
name = Atom.to_string(key),
|
||||
ctx.matcher.(name, hint),
|
||||
do: {:map_field, name, value}
|
||||
do: {:map_field, key, value}
|
||||
end
|
||||
|
||||
defp match_sigil(hint, ctx) do
|
||||
|
|
|
|||
|
|
@ -1053,6 +1053,52 @@ defmodule Livebook.IntellisenseTest do
|
|||
] = Intellisense.get_completion_items("struct.my", binding, env)
|
||||
end
|
||||
|
||||
test "completion for struct keys inside struct" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
assert [
|
||||
%{
|
||||
label: "my_val",
|
||||
kind: :field,
|
||||
detail: "Livebook.IntellisenseTest.MyStruct struct field",
|
||||
documentation: "```\nmy_val\n```\n\n---\n\n**Default**\n\n```\nnil\n```\n",
|
||||
insert_text: "my_val: "
|
||||
}
|
||||
] =
|
||||
Intellisense.get_completion_items(
|
||||
"%Livebook.IntellisenseTest.MyStruct{my",
|
||||
binding,
|
||||
env
|
||||
)
|
||||
end
|
||||
|
||||
test "completion for struct keys inside struct removes filled keys" do
|
||||
{binding, env} =
|
||||
eval do
|
||||
struct = %Livebook.IntellisenseTest.MyStruct{}
|
||||
end
|
||||
|
||||
assert [] =
|
||||
Intellisense.get_completion_items(
|
||||
"%Livebook.IntellisenseTest.MyStruct{my_val: 123, ",
|
||||
binding,
|
||||
env
|
||||
)
|
||||
end
|
||||
|
||||
test "completion for struct keys inside struct ignores `__exception__`" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
completions =
|
||||
Intellisense.get_completion_items(
|
||||
"%ArgumentError{",
|
||||
binding,
|
||||
env
|
||||
)
|
||||
|
||||
refute Enum.find(completions, &match?(%{label: "__exception__"}, &1))
|
||||
end
|
||||
|
||||
test "ignore invalid Elixir module literals" do
|
||||
{binding, env} = eval(do: nil)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue