@@ -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
@@ -722,18 +741,37 @@ defmodule ElixirLS.Utils.CompletionEngine do
722741 end
723742
724743 defp struct_module_filter ( true , % State.Env { } = _env , % Metadata { } = metadata ) do
744+ # Build the loaded/application module-name list ONCE for the whole filter pass
745+ # rather than recomputing `:code.all_loaded()` (and the application module scan)
746+ # for every candidate module - that turned the filter into O(N^2) work.
747+ all_module_names = all_module_names ( )
748+
725749 fn module ->
726750 Struct . is_struct ( module , metadata . structs ) or
727- has_struct_submodule? ( module , metadata . structs )
751+ has_struct_submodule? ( module , metadata . structs , all_module_names )
728752 end
729753 end
730754
731755 defp struct_module_filter ( false , % State.Env { } = _env , % Metadata { } = _metadata ) do
732756 fn _ -> true end
733757 end
734758
759+ # The loaded (and, in interactive mode, application) module names as strings.
760+ # Computed once per struct-module filter pass, not per candidate module.
761+ defp all_module_names do
762+ modules = Enum . map ( :code . all_loaded ( ) , & Atom . to_string ( elem ( & 1 , 0 ) ) )
763+
764+ case :code . get_mode ( ) do
765+ :interactive ->
766+ modules ++ Enum . map ( Applications . get_modules_from_applications ( ) , & Atom . to_string / 1 )
767+
768+ _ ->
769+ modules
770+ end
771+ end
772+
735773 # Check if a module has any direct submodules that are structs
736- defp has_struct_submodule? ( module , structs ) do
774+ defp has_struct_submodule? ( module , structs , all_module_names ) do
737775 module_str = Atom . to_string ( module )
738776
739777 # Check metadata structs (from current buffer)
@@ -747,30 +785,14 @@ defmodule ElixirLS.Utils.CompletionEngine do
747785 if metadata_result do
748786 true
749787 else
750- # Get all modules and check if any direct submodule is a struct
788+ # Check if any direct submodule (from the precomputed list) is a struct
751789 module_str_with_dot = module_str <> "."
752790
753- # Get all loaded modules
754- modules = Enum . map ( :code . all_loaded ( ) , & Atom . to_string ( elem ( & 1 , 0 ) ) )
755-
756- # Add modules from applications if in interactive mode
757- modules =
758- case :code . get_mode ( ) do
759- :interactive ->
760- modules ++
761- Enum . map ( Applications . get_modules_from_applications ( ) , & Atom . to_string / 1 )
762-
763- _ ->
764- modules
765- end
766-
767- # Find submodules
768791 submodules =
769- for mod <- modules ,
792+ for mod <- all_module_names ,
770793 String . starts_with? ( mod , module_str_with_dot ) ,
771794 do: String . to_atom ( mod )
772795
773- # Check if any submodule is a struct
774796 Enum . any? ( submodules , fn mod ->
775797 Code . ensure_loaded? ( mod ) and function_exported? ( mod , :__struct__ , 1 )
776798 end )
@@ -793,16 +815,22 @@ defmodule ElixirLS.Utils.CompletionEngine do
793815 { container_context_map_fields ( pairs , { :struct , alias } , map , hint , metadata ) , continue? }
794816
795817 :bitstring_modifier ->
796- 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 =
797823 code
798824 |> List . to_string ( )
799825 |> String . split ( "::" )
800826 |> List . last ( )
801- |> String . split ( "-" )
827+ |> bitstring_parse ( )
828+ |> bitstring_available_options ( )
829+ |> Enum . map ( & Atom . to_string / 1 )
802830
803831 results =
804832 @ bitstring_modifiers
805- |> 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 ) ) )
806834 |> format_expansion ( )
807835
808836 { results , false }
@@ -812,6 +840,92 @@ defmodule ElixirLS.Utils.CompletionEngine do
812840 end
813841 end
814842
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+
815929 defp container_context_map_fields ( pairs , kind , map , hint , metadata ) do
816930 { keys , types , alias , doc , meta } =
817931 case kind do
0 commit comments