-
-
Notifications
You must be signed in to change notification settings - Fork 83
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Improve completions with default arguments
A couple of updates ago, elixir sense started returning the defaults of arguments along with the names of the arguments. This make our completions much worse, as the arity of the completion didn't match the arity of the function selected *and* the defaults would get pasted into the completion. This meant you had to remove the default from the completion, which was beyond annoying. These changes strip out defaults and ensure that the arity and arguments match the completion result generated by elixir sense.
- Loading branch information
Showing
6 changed files
with
234 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
80 changes: 80 additions & 0 deletions
80
apps/remote_control/lib/lexical/remote_control/completion/result/argument_names.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
defmodule Lexical.RemoteControl.Completion.Result.ArgumentNames do | ||
@moduledoc """ | ||
Elixir sense, for whatever reason returns all the argument names when asked to do a completion on a function. | ||
This means that the arity of the function might differ from the argument names returned. Furthermore, the | ||
argument names have the defaults in them for some reason, which is completely unhelpful for completions. | ||
The functions below match the arity of the argument names to the given arity, and strip out the defaults, | ||
rendering the results useful for completion. | ||
""" | ||
@type argument_names :: [String.t()] | ||
@default_specifier "\\\\" | ||
|
||
@spec from_elixir_sense_map(map()) :: :error | argument_names() | ||
def from_elixir_sense_map(%{args_list: argument_names, arity: arity}) do | ||
from_elixir_sense(argument_names, arity) | ||
end | ||
|
||
def from_elixir_sense_map(%{}) do | ||
:error | ||
end | ||
|
||
@spec from_elixir_sense([String.t()], non_neg_integer) :: :error | argument_names() | ||
def from_elixir_sense(argument_names, arity) when is_list(argument_names) do | ||
{names, required_count} = preprocess(argument_names) | ||
|
||
cond do | ||
length(argument_names) < arity -> | ||
:error | ||
|
||
arity < required_count -> | ||
:error | ||
|
||
true -> | ||
generate_argument_list(names, required_count, arity) | ||
end | ||
end | ||
|
||
defp generate_argument_list(names, required_count, arity) do | ||
optional_count = arity - required_count | ||
|
||
{arg_list, _} = | ||
Enum.reduce(names, {[], optional_count}, fn | ||
{:required, arg_name}, {arg_list, optional_count} -> | ||
{[arg_name | arg_list], optional_count} | ||
|
||
{:optional, _}, {_arg_list, 0} = acc -> | ||
acc | ||
|
||
{:optional, arg_name}, {arg_list, optional_count} -> | ||
{[arg_name | arg_list], optional_count - 1} | ||
end) | ||
|
||
Enum.reverse(arg_list) | ||
end | ||
|
||
defp split_on_default(argument) do | ||
argument | ||
|> String.split(@default_specifier) | ||
|> Enum.map(&String.trim/1) | ||
end | ||
|
||
@spec preprocess(argument_names()) :: {[String.t()], non_neg_integer()} | ||
defp preprocess(argument_names) do | ||
{names, required_count} = | ||
Enum.reduce(argument_names, {[], 0}, fn argument, {names, required_count} -> | ||
{name, increment} = | ||
case split_on_default(argument) do | ||
[argument_name] -> | ||
{{:required, argument_name}, 1} | ||
|
||
[argument_name, _default] -> | ||
{{:optional, argument_name}, 0} | ||
end | ||
|
||
{[name | names], required_count + increment} | ||
end) | ||
|
||
{Enum.reverse(names), required_count} | ||
end | ||
end |
21 changes: 21 additions & 0 deletions
21
apps/remote_control/test/fixtures/project/lib/default_args.ex
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
defmodule Project.DefaultArgs do | ||
def first_arg(x \\ 3, y) do | ||
x + y | ||
end | ||
|
||
def middle_arg(a, b \\ 3, c) do | ||
a + b + c | ||
end | ||
|
||
def last_arg(x, y \\ 3) do | ||
x + y | ||
end | ||
|
||
def options(a, opts \\ []) do | ||
a + Keyword.get(opts, :b, 0) | ||
end | ||
|
||
def struct_arg(a, b \\ %Project.Structs.User{}) do | ||
Map.put(b, :username, a) | ||
end | ||
end |
56 changes: 56 additions & 0 deletions
56
apps/remote_control/test/lexical/remote_control/completion/result/argument_names_test.exs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
defmodule Lexical.RemoteControl.Completion.Result.ArgumentNamesTest do | ||
alias Lexical.RemoteControl.Completion.Result.ArgumentNames | ||
|
||
use ExUnit.Case | ||
import ArgumentNames | ||
|
||
describe "parsing elixir sense argument names" do | ||
test "handles normal arguments" do | ||
assert ~w(first second third) == from_elixir_sense(~w(first second third), 3) | ||
end | ||
|
||
test "handles default arguments in the first position" do | ||
args = ["first \\\\ 3", "second", "third"] | ||
|
||
assert ~w(second third) == from_elixir_sense(args, 2) | ||
assert ~w(first second third) == from_elixir_sense(args, 3) | ||
end | ||
|
||
test "handles default arguments in the middle" do | ||
args = ["first", "second \\\\", "third"] | ||
|
||
assert ~w(first third) = from_elixir_sense(args, 2) | ||
assert ~w(first second third) = from_elixir_sense(args, 3) | ||
end | ||
|
||
test "handles default arguments at the last position" do | ||
args = ["first", "second", "third \\\\ 3"] | ||
|
||
assert ~w(first second) = from_elixir_sense(args, 2) | ||
assert ~w(first second third) = from_elixir_sense(args, 3) | ||
end | ||
|
||
test "handles multiple default arguments" do | ||
args = ["first", "second \\\\ 1", "third", "fourth \\\\ 2", "fifth \\\\ 3", "sixth"] | ||
|
||
assert ~w(first third sixth) = from_elixir_sense(args, 3) | ||
assert ~w(first second third sixth) = from_elixir_sense(args, 4) | ||
assert ~w(first second third fourth sixth) = from_elixir_sense(args, 5) | ||
assert ~w(first second third fourth fifth sixth) = from_elixir_sense(args, 6) | ||
end | ||
|
||
test "handles struct defaults" do | ||
args = ["first", "second \\\\ %Struct{}", "third"] | ||
|
||
assert ~w(first third) = from_elixir_sense(args, 2) | ||
assert ~w(first second third) = from_elixir_sense(args, 3) | ||
end | ||
|
||
test "handles incorrect arity" do | ||
args = ~w(first second third) | ||
assert :error == from_elixir_sense(args, 1) | ||
assert :error == from_elixir_sense(args, 2) | ||
assert :error == from_elixir_sense(args, 4) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters