Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve completions #410

Merged
merged 11 commits into from
Oct 16, 2023
Prev Previous commit
Next Next commit
Refactor struct field completion context
  • Loading branch information
zachallaun committed Oct 16, 2023
commit 7c48883a2311b8865768256d7c9127b9dcd93975
35 changes: 15 additions & 20 deletions apps/common/lib/lexical/ast/env.ex
Original file line number Diff line number Diff line change
@@ -132,31 +132,26 @@ defmodule Lexical.Ast.Env do
end
end

defp do_in_context?(env, :struct_arguments) do
defp do_in_context?(env, :struct_fields) do
env.document
|> Ast.cursor_path(env.position)
|> Enum.any?(&match?({:%, _, _}, &1))
end

defp do_in_context?(env, :struct_field_key) do
cursor_path = Ast.cursor_path(env.document, env.position)

Enum.any?(cursor_path, fn
# struct leading by current module: `%__MODULE__.Struct{|}`
# or leading by a module alias: `%Alias.Struct{|}`
# or just a struct: `%Struct{|}`
{:%, _, [{:__aliases__, _, _aliases} | _]} -> true
# current module struct: `%__MODULE__{|}`
{:%, _, [{:__MODULE__, _, _} | _]} -> true
_ -> false
end)
match?(
# in the key position, the cursor will always be followed by the
# map node because, in any other case, there will minimally be a
# 2-element key-value tuple containing the cursor
[{:__cursor__, _, _}, {:%{}, _, _}, {:%, _, _} | _],
cursor_path
)
end

defp do_in_context?(env, :struct_field_value) do
if do_in_context?(env, :struct_arguments) do
env
|> prefix_tokens(2)
|> Enum.any?(fn
{:kw_identifier, _, _} -> true
_ -> false
end)
else
false
end
do_in_context?(env, :struct_fields) and not do_in_context?(env, :struct_field_key)
end

defp do_in_context?(env, :pipe) do
10 changes: 9 additions & 1 deletion apps/common/lib/lexical/ast/environment.ex
Original file line number Diff line number Diff line change
@@ -6,7 +6,15 @@ defmodule Lexical.Ast.Environment do
@type lexer_token :: {atom, token_value}
@type token_count :: pos_integer | :all

@type context_type :: :pipe | :alias | :struct_reference | :function_capture | :bitstring
@type context_type ::
:pipe
| :alias
| :struct_reference
| :struct_fields
| :struct_field_key
| :struct_field_value
| :function_capture
| :bitstring

@callback in_context?(t, context_type) :: boolean

78 changes: 66 additions & 12 deletions apps/common/test/lexical/ast/env_test.exs
Original file line number Diff line number Diff line change
@@ -221,7 +221,7 @@ defmodule Lexical.Ast.EnvTest do
end
end

describe "in_context?(env, :struct_arguments)" do
describe "in_context?(env, :struct_fields)" do
def wrap_with_module(text) do
"""
defmodule MyModule do
@@ -247,42 +247,42 @@ defmodule Lexical.Ast.EnvTest do

test "is true if the cursor is directly after the opening curly" do
env = "%User{|}" |> wrap_with_module() |> new_env()
assert in_context?(env, :struct_arguments)
assert in_context?(env, :struct_fields)
end

test "is true when the struct is in the function variable" do
env = "%User{|}" |> wrap_with_function() |> wrap_with_module() |> new_env()
assert in_context?(env, :struct_arguments)
assert in_context?(env, :struct_fields)
end

test "is true when the struct is in the function arguments" do
env = "%User{|}" |> wrap_with_function_arguments() |> wrap_with_module() |> new_env()
assert in_context?(env, :struct_arguments)
assert in_context?(env, :struct_fields)
end

test "is true if the cursor is after the field name" do
env = "%User{name: |}" |> wrap_with_module() |> new_env()
assert in_context?(env, :struct_arguments)
assert in_context?(env, :struct_fields)
end

test "is true if the cursor is after the field value" do
env = "%User{name: \"John\"|}" |> wrap_with_module() |> new_env()
assert in_context?(env, :struct_arguments)
assert in_context?(env, :struct_fields)
end

test "is true if the cursor starts in the middle of the struct" do
env = "%User{name: \"John\", |}" |> wrap_with_module() |> new_env()
assert in_context?(env, :struct_arguments)
assert in_context?(env, :struct_fields)
end

test "is false if the cursor is after the closing curly" do
env = "%User{}|" |> wrap_with_module() |> new_env()
refute in_context?(env, :struct_arguments)
refute in_context?(env, :struct_fields)
end

test "is true if the cursor is in current module arguments" do
env = "%__MODULE__{|}" |> wrap_with_function() |> wrap_with_module() |> new_env()
assert in_context?(env, :struct_arguments)
assert in_context?(env, :struct_fields)
end

test "is true if the struct alias spans multiple lines" do
@@ -293,17 +293,22 @@ defmodule Lexical.Ast.EnvTest do
}
]
env = new_env(source)
assert in_context?(env, :struct_arguments)
assert in_context?(env, :struct_fields)
end

test "is true even if the value of a struct key is a tuple" do
env = new_env("%User{favorite_numbers: {3}|")
assert in_context?(env, :struct_arguments)
assert in_context?(env, :struct_fields)
end

test "is true even if the cursor is at a nested struct" do
env = new_env("%User{address: %Address{}|")
assert in_context?(env, :struct_arguments)
assert in_context?(env, :struct_fields)
zachallaun marked this conversation as resolved.
Show resolved Hide resolved
end

test "is false if the cursor is in a map" do
env = "%{|field: value}" |> wrap_with_module() |> new_env()
refute in_context?(env, :struct_fields)
end
end

@@ -346,6 +351,55 @@ defmodule Lexical.Ast.EnvTest do
end
end

describe "in_context?(env, :struct_field_key)" do
test "is true if the cursor is after the struct opening" do
env = new_env("%User{|}")
assert in_context?(env, :struct_field_key)
end

test "is true if a key is partially typed" do
env = new_env("%User{fo|}")
assert in_context?(env, :struct_field_key)
end

test "is true if after a comma" do
env = new_env("%User{foo: 1, |}")
assert in_context?(env, :struct_field_key)
end

test "is true if after a comma on another line" do
source = ~q[
%User{
foo: 1,
|
}
]

env = new_env(source)
assert in_context?(env, :struct_field_key)
end

test "is false in static keywords" do
env = "[fo|]" |> wrap_with_module() |> new_env()
refute in_context?(env, :struct_field_key)
end

test "is false in static keywords nested in a struct" do
env = "%User{foo: [fo|]}" |> wrap_with_module() |> new_env()
refute in_context?(env, :struct_field_key)
end

test "is false in map field key position" do
env = "%{|}" |> wrap_with_module() |> new_env()
refute in_context?(env, :struct_field_key)
end

test "is false in map field key position nested in a struct" do
env = "%User{foo: %{|}}" |> wrap_with_module() |> new_env()
refute in_context?(env, :struct_field_key)
end
end

describe "in_context?(env, :struct_reference)" do
test "is true if the reference starts on the beginning of the line" do
env = new_env("%User|")
13 changes: 1 addition & 12 deletions apps/server/lib/lexical/server/code_intelligence/completion.ex
Original file line number Diff line number Diff line change
@@ -60,8 +60,7 @@ defmodule Lexical.Server.CodeIntelligence.Completion do
|> Builder.snippet(do_end_snippet, label: "do/end block")
|> List.wrap()

Env.in_context?(env, :struct_arguments) and not Env.in_context?(env, :struct_field_value) and
not prefix_is_trigger?(env) ->
Env.in_context?(env, :struct_field_key) ->
project
|> RemoteControl.Api.complete_struct_fields(env.document, env.position)
|> Enum.map(&Translatable.translate(&1, Builder, env))
zachallaun marked this conversation as resolved.
Show resolved Hide resolved
@@ -91,16 +90,6 @@ defmodule Lexical.Server.CodeIntelligence.Completion do
valid_prefix? and Env.empty?(env.suffix)
end

defp prefix_is_trigger?(%Env{} = env) do
case Env.prefix_tokens(env, 1) do
[{_, token, _}] ->
to_string(token) in trigger_characters()

_ ->
false
end
end

defp to_completion_items(
local_completions,
%Project{} = project,
Original file line number Diff line number Diff line change
@@ -172,7 +172,7 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.StructFieldTes
assert apply_completion(completion) == expected
end

test "should complete even the struct module is aliased", %{project: project} do
test "should complete when the struct module is aliased", %{project: project} do
source = ~q[
defmodule MyModule do
alias Project.Structs.Account, as: LocalAccount
@@ -209,7 +209,7 @@ defmodule Lexical.Server.CodeIntelligence.Completion.Translations.StructFieldTes
|> fetch_completion(kind: :field)
end

test "complete nothing when the prefix is a tigger", %{project: project} do
test "complete nothing when the prefix is invalid", %{project: project} do
assert {:error, :not_found} ==
project
|> complete("%Project.Structs.Account{l.|}")