Skip to content

Commit c0f7245

Browse files
lukaszsamsonclaude
andcommitted
completion: restore state-based bitstring modifier filtering; bump elixir_sense
Mirror the elixir_sense fix in the ElixirLS.Utils.CompletionEngine copy: the `:bitstring_modifier` branch now parses the modifiers already typed after `::` into a state and keeps only the modifiers that may still legally follow (type / sign / endianness / utf / size / unit rules), instead of only dropping names already present. Fixes invalid suggestions like `signed`/`little` after `binary-`, `size`/`unit` after `utf8-`, `signed`/`binary` after `float-`. Assert the invalid options are absent (not just valid ones present) in both elixir_ls_utils (complete_test) and language_server (suggestions_test) suites. Bump elixir_sense to 89eda3fc (same fix in the shared engine). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XRd15BX6wBpZ8ThaNSo232
1 parent 45271eb commit c0f7245

5 files changed

Lines changed: 189 additions & 5 deletions

File tree

apps/elixir_ls_utils/lib/completion_engine.ex

Lines changed: 114 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,25 @@ defmodule ElixirLS.Utils.CompletionEngine do
9797
%{type: :bitstring_option, name: "utf32"}
9898
]
9999

100+
# Bitstring modifier validity rules (UTS of `<<x::type-sign-endianness-size-unit>>`). Used to keep
101+
# `:bitstring_modifier` completion from offering combinations Elixir rejects (e.g. `signed` after
102+
# `binary-`, `size`/`unit` after `utf8-`). Inlined here rather than pulled from a separate module so
103+
# the completion engine stays self-contained.
104+
@bitstring_types [:integer, :float, :bitstring, :binary, :utf8, :utf16, :utf32]
105+
@bitstring_utf_types [:utf8, :utf16, :utf32]
106+
@bitstring_type_aliases [bits: :bitstring, bytes: :binary]
107+
@bitstring_sign_modifiers [:signed, :unsigned]
108+
@bitstring_endianness_modifiers [:little, :big, :native]
109+
# types each endianness modifier is compatible with, when the type is not yet fixed
110+
@bitstring_endianness_types [:integer, :float, :utf16, :utf32]
111+
@bitstring_default_state %{
112+
type: nil,
113+
sign_modifier: nil,
114+
endianness_modifier: nil,
115+
size: nil,
116+
unit: nil
117+
}
118+
100119
@alias_only_atoms ~w(alias import require)a
101120
@alias_only_charlists ~w(alias import require)c
102121

@@ -796,16 +815,22 @@ defmodule ElixirLS.Utils.CompletionEngine do
796815
{container_context_map_fields(pairs, {:struct, alias}, map, hint, metadata), continue?}
797816

798817
:bitstring_modifier ->
799-
existing =
818+
# Parse the modifiers already typed after `::` into a state and keep only the modifiers that
819+
# may still legally follow (honoring the type / sign / endianness / utf / size / unit rules),
820+
# instead of merely excluding names already present - which would offer invalid combinations
821+
# such as `signed`/`little` after `binary-`, or `size`/`unit`/`signed` after `utf8-`.
822+
available_names =
800823
code
801824
|> List.to_string()
802825
|> String.split("::")
803826
|> List.last()
804-
|> String.split("-")
827+
|> bitstring_parse()
828+
|> bitstring_available_options()
829+
|> Enum.map(&Atom.to_string/1)
805830

806831
results =
807832
@bitstring_modifiers
808-
|> Enum.filter(&(Matcher.match?(&1.name, hint) and &1.name not in existing))
833+
|> Enum.filter(&(&1.name in available_names and Matcher.match?(&1.name, hint)))
809834
|> format_expansion()
810835

811836
{results, false}
@@ -815,6 +840,92 @@ defmodule ElixirLS.Utils.CompletionEngine do
815840
end
816841
end
817842

843+
# Parse the modifier text already typed after `::` (e.g. `binary-siz`) into a
844+
# `%{type, sign_modifier, endianness_modifier, size, unit}` state. Unknown/partial characters (the
845+
# in-progress hint) are skipped one at a time, so a complete parse is not required.
846+
defp bitstring_parse(text), do: bitstring_parse(text, @bitstring_default_state)
847+
848+
for type <- @bitstring_types do
849+
defp bitstring_parse(<<unquote(Atom.to_string(type)), rest::binary>>, acc),
850+
do: bitstring_parse(rest, %{acc | type: unquote(type)})
851+
end
852+
853+
for {type_alias, type} <- @bitstring_type_aliases do
854+
defp bitstring_parse(<<unquote(Atom.to_string(type_alias)), rest::binary>>, acc),
855+
do: bitstring_parse(rest, %{acc | type: unquote(type)})
856+
end
857+
858+
for sign <- @bitstring_sign_modifiers do
859+
defp bitstring_parse(<<unquote(Atom.to_string(sign)), rest::binary>>, acc),
860+
do: bitstring_parse(rest, %{acc | sign_modifier: unquote(sign)})
861+
end
862+
863+
for endianness <- @bitstring_endianness_modifiers do
864+
defp bitstring_parse(<<unquote(Atom.to_string(endianness)), rest::binary>>, acc),
865+
do: bitstring_parse(rest, %{acc | endianness_modifier: unquote(endianness)})
866+
end
867+
868+
defp bitstring_parse(<<"-", rest::binary>>, acc), do: bitstring_parse(rest, acc)
869+
870+
defp bitstring_parse(<<"size", rest::binary>>, acc),
871+
do: bitstring_parse(rest, %{acc | size: true})
872+
873+
defp bitstring_parse(<<"unit", rest::binary>>, acc),
874+
do: bitstring_parse(rest, %{acc | unit: true})
875+
876+
defp bitstring_parse(<<_::binary-size(1), rest::binary>>, acc), do: bitstring_parse(rest, acc)
877+
defp bitstring_parse(<<>>, acc), do: acc
878+
879+
# The modifiers that may still legally follow, given the parsed state.
880+
defp bitstring_available_options(state) do
881+
bitstring_available_types(state) ++
882+
bitstring_available_sign_modifiers(state) ++
883+
bitstring_available_endianness_modifiers(state) ++
884+
bitstring_available_size(state) ++
885+
bitstring_available_unit(state)
886+
end
887+
888+
defp bitstring_available_types(
889+
%{type: nil, sign_modifier: nil, endianness_modifier: nil} = state
890+
),
891+
do: bitstring_filter_utf(state, @bitstring_types)
892+
893+
defp bitstring_available_types(%{type: nil, sign_modifier: nil, endianness_modifier: e} = state)
894+
when not is_nil(e),
895+
do: bitstring_filter_utf(state, @bitstring_endianness_types)
896+
897+
defp bitstring_available_types(%{type: nil}), do: [:integer]
898+
defp bitstring_available_types(_), do: []
899+
900+
defp bitstring_available_sign_modifiers(%{type: type, sign_modifier: nil})
901+
when type in [nil, :integer],
902+
do: @bitstring_sign_modifiers
903+
904+
defp bitstring_available_sign_modifiers(_), do: []
905+
906+
defp bitstring_available_endianness_modifiers(%{type: type, endianness_modifier: nil})
907+
when type in [nil, :integer, :utf16, :utf32],
908+
do: @bitstring_endianness_modifiers
909+
910+
defp bitstring_available_endianness_modifiers(%{type: :float, endianness_modifier: nil}),
911+
do: [:little, :big]
912+
913+
defp bitstring_available_endianness_modifiers(_), do: []
914+
915+
# size and unit are unsupported on utf types (they fail to compile).
916+
defp bitstring_available_size(%{size: nil, type: type}) when type not in @bitstring_utf_types,
917+
do: [:size]
918+
919+
defp bitstring_available_size(_), do: []
920+
921+
defp bitstring_available_unit(%{unit: nil, type: type}) when type not in @bitstring_utf_types,
922+
do: [:unit]
923+
924+
defp bitstring_available_unit(_), do: []
925+
926+
defp bitstring_filter_utf(%{size: nil, unit: nil}, list), do: list
927+
defp bitstring_filter_utf(_, list), do: list -- @bitstring_utf_types
928+
818929
defp container_context_map_fields(pairs, kind, map, hint, metadata) do
819930
{keys, types, alias, doc, meta} =
820931
case kind do

apps/elixir_ls_utils/test/complete_test.exs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2033,6 +2033,32 @@ defmodule ElixirLS.Utils.CompletionEngineTest do
20332033
refute Enum.any?(entries, &(&1.name == "integer"))
20342034
refute Enum.any?(entries, &(&1.name == "little"))
20352035
assert Enum.any?(entries, &(&1.name == "size" and &1.arity == 1))
2036+
2037+
# state-based validity: a `binary` type only allows size/unit to follow
2038+
names = fn code ->
2039+
expand(code) |> Enum.filter(&(&1[:type] == :bitstring_option)) |> Enum.map(& &1.name)
2040+
end
2041+
2042+
binary = names.('<<foo::binary-')
2043+
assert "size" in binary
2044+
assert "unit" in binary
2045+
refute "signed" in binary
2046+
refute "little" in binary
2047+
refute "native" in binary
2048+
refute "utf8" in binary
2049+
2050+
# utf types take no further modifiers
2051+
assert names.('<<foo::utf8-') == []
2052+
2053+
# float allows little/big (not native), size and unit - but not sign or another type
2054+
float = names.('<<foo::float-')
2055+
assert "little" in float
2056+
assert "big" in float
2057+
assert "size" in float
2058+
assert "unit" in float
2059+
refute "native" in float
2060+
refute "signed" in float
2061+
refute "binary" in float
20362062
end
20372063

20382064
test "completion for aliases in special forms" do

apps/language_server/test/providers/completion/suggestions_test.exs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4765,6 +4765,53 @@ defmodule ElixirLS.LanguageServer.Providers.Completion.SuggestionTest do
47654765

47664766
assert "unit" in options
47674767
assert "size" in options
4768+
# invalid after a `binary` type: sign / endianness / another type must not be offered
4769+
refute "signed" in options
4770+
refute "unsigned" in options
4771+
refute "little" in options
4772+
refute "big" in options
4773+
refute "native" in options
4774+
refute "utf8" in options
4775+
refute "binary" in options
4776+
4777+
# utf types take no further modifiers - size/unit/sign/endianness are all invalid
4778+
buffer = """
4779+
defmodule ElixirSenseExample.OtherModule do
4780+
alias ElixirSenseExample.SameModule
4781+
def some_fun() do
4782+
<<abc::integer, asd::utf8->>
4783+
end
4784+
end
4785+
"""
4786+
4787+
assert [] =
4788+
Suggestion.suggestions(buffer, 4, 31)
4789+
|> Enum.filter(&(&1.type == :bitstring_option))
4790+
|> Enum.map(& &1.name)
4791+
4792+
# float supports little/big (not native), size and unit - but not sign or another type
4793+
buffer = """
4794+
defmodule ElixirSenseExample.OtherModule do
4795+
alias ElixirSenseExample.SameModule
4796+
def some_fun() do
4797+
<<abc::integer, asd::float->>
4798+
end
4799+
end
4800+
"""
4801+
4802+
options =
4803+
Suggestion.suggestions(buffer, 4, 32)
4804+
|> Enum.filter(&(&1.type == :bitstring_option))
4805+
|> Enum.map(& &1.name)
4806+
4807+
assert "little" in options
4808+
assert "big" in options
4809+
assert "size" in options
4810+
assert "unit" in options
4811+
refute "native" in options
4812+
refute "signed" in options
4813+
refute "binary" in options
4814+
refute "utf8" in options
47684815

47694816
buffer = """
47704817
defmodule ElixirSenseExample.OtherModule do

dep_versions.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[
2-
elixir_sense: "99570b9b749dbdde5120e22963c95298365511a0",
2+
elixir_sense: "89eda3fc4df9cea0d70c528f3e7e240a70c860f8",
33
dialyxir_vendored: "accfec9393079abc4a82b7e79a4997f59f085b67",
44
jason_v: "f1c10fa9c445cb9f300266122ef18671054b2330",
55
erl2ex_vendored: "04f93e55f46d35d0aa3c149616f2c7a6a1ad9311",

mix.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"benchee": {:hex, :benchee, "1.1.0", "f3a43817209a92a1fade36ef36b86e1052627fd8934a8b937ac9ab3a76c43062", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}], "hexpm", "7da57d545003165a012b587077f6ba90b89210fd88074ce3c60ce239eb5e6d93"},
33
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},
44
"dialyxir_vendored": {:git, "https://github.com/elixir-lsp/dialyxir.git", "accfec9393079abc4a82b7e79a4997f59f085b67", [ref: "accfec9393079abc4a82b7e79a4997f59f085b67"]},
5-
"elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "99570b9b749dbdde5120e22963c95298365511a0", [ref: "99570b9b749dbdde5120e22963c95298365511a0"]},
5+
"elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "89eda3fc4df9cea0d70c528f3e7e240a70c860f8", [ref: "89eda3fc4df9cea0d70c528f3e7e240a70c860f8"]},
66
"erl2ex_vendored": {:git, "https://github.com/elixir-lsp/erl2ex.git", "04f93e55f46d35d0aa3c149616f2c7a6a1ad9311", [ref: "04f93e55f46d35d0aa3c149616f2c7a6a1ad9311"]},
77
"erlex_vendored": {:git, "https://github.com/elixir-lsp/erlex.git", "50b8307f90451a5d0288fb239fb6405b5ca1f1a4", [ref: "50b8307f90451a5d0288fb239fb6405b5ca1f1a4"]},
88
"jason_v": {:git, "https://github.com/elixir-lsp/jason.git", "f1c10fa9c445cb9f300266122ef18671054b2330", [ref: "f1c10fa9c445cb9f300266122ef18671054b2330"]},

0 commit comments

Comments
 (0)