-
-
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.
* Code Action: Add alias When coding, it is more convenient to type what you mean, and then come back later and clean up things like aliases. This change implements a code action that understands your current editing context and adds aliases as you type. It finds suggestions using our fuzzy matcher (though it has a bit stricter fuzziness), then provides code actions for each suggestion. It will fix the following alias types: `Foo.Bar.Baz|` will result in aliases for modules similar to `Baz` `%Foo.Bar.Baz|{}` will result in aliases similar to `Baz`, but only those that have structs defined in them `Foo.Bar.baz.function|` will result in aliases for modules with a function with a name similar to `function` This PR also pulled a bunch of common code out of `orgaize_aliases` and moved it to a code mod file, which resulted in `organize_aliases` having very little code left in it. Fixes #716 We couldn't use the existing fuzzy matcher, which excludes dependencies, which meant that you couldn't add an alias to a dependency's modules. Instead, we now build a new fuzzy matcher with all available modules, and match against that.
- Loading branch information
Showing
10 changed files
with
1,004 additions
and
137 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
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
211 changes: 211 additions & 0 deletions
211
apps/remote_control/lib/lexical/remote_control/code_action/handlers/add_alias.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,211 @@ | ||
defmodule Lexical.RemoteControl.CodeAction.Handlers.AddAlias do | ||
alias Lexical.Ast | ||
alias Lexical.Ast.Analysis | ||
alias Lexical.Ast.Analysis.Alias | ||
alias Lexical.Document | ||
alias Lexical.Document.Changes | ||
alias Lexical.Document.Position | ||
alias Lexical.Document.Range | ||
alias Lexical.Formats | ||
alias Lexical.RemoteControl | ||
alias Lexical.RemoteControl.Analyzer | ||
alias Lexical.RemoteControl.CodeAction | ||
alias Lexical.RemoteControl.CodeIntelligence.Entity | ||
alias Lexical.RemoteControl.CodeMod | ||
alias Lexical.RemoteControl.Modules | ||
alias Lexical.RemoteControl.Search.Fuzzy | ||
alias Lexical.RemoteControl.Search.Indexer.Entry | ||
alias Mix.Tasks.Namespace | ||
alias Sourceror.Zipper | ||
|
||
@behaviour CodeAction.Handler | ||
|
||
@impl CodeAction.Handler | ||
def actions(%Document{} = doc, %Range{} = range, _diagnostics) do | ||
with {:ok, _doc, %Analysis{valid?: true} = analysis} <- | ||
Document.Store.fetch(doc.uri, :analysis), | ||
{:ok, resolved, _} <- Entity.resolve(analysis, range.start), | ||
{:ok, unaliased_module} <- fetch_unaliased_module(analysis, range.start, resolved) do | ||
current_aliases = CodeMod.Aliases.in_scope(analysis, range) | ||
|
||
unaliased_module | ||
|> possible_aliases() | ||
|> filter_by_resolution(resolved) | ||
|> Stream.map(&build_code_action(analysis, range, current_aliases, &1)) | ||
|> Enum.reject(&is_nil/1) | ||
else | ||
_ -> | ||
[] | ||
end | ||
end | ||
|
||
@impl CodeAction.Handler | ||
def kinds do | ||
[:quick_fix] | ||
end | ||
|
||
defp build_code_action(%Analysis{} = analysis, range, current_aliases, potential_alias_module) do | ||
case Ast.Module.safe_split(potential_alias_module, as: :atoms) do | ||
{:erlang, _} -> | ||
nil | ||
|
||
{:elixir, segments} -> | ||
{insert_position, trailer} = CodeMod.Aliases.insert_position(analysis, range.start) | ||
alias_to_add = %Alias{module: segments, as: List.last(segments), explicit?: true} | ||
replace_current_alias = get_current_replacement(analysis, range, segments) | ||
|
||
alias_edits = | ||
CodeMod.Aliases.to_edits( | ||
[alias_to_add | current_aliases], | ||
insert_position, | ||
trailer | ||
) | ||
|
||
changes = Changes.new(analysis.document, replace_current_alias ++ alias_edits) | ||
|
||
CodeAction.new( | ||
analysis.document.uri, | ||
"alias #{Formats.module(potential_alias_module)}", | ||
:quick_fix, | ||
changes | ||
) | ||
end | ||
end | ||
|
||
def fetch_unaliased_module(%Analysis{} = analysis, %Position{} = position, resolved) do | ||
with {:ok, module} <- fetch_module(resolved), | ||
%{} = aliases <- Analyzer.aliases_at(analysis, position), | ||
false <- module in Map.values(aliases) do | ||
{:ok, module} | ||
else | ||
_ -> | ||
:error | ||
end | ||
end | ||
|
||
defp fetch_module({:module, module}), do: {:ok, module} | ||
defp fetch_module({:struct, module}), do: {:ok, module} | ||
defp fetch_module({:call, module, _function, _arity}), do: {:ok, module} | ||
defp fetch_module(_), do: :error | ||
|
||
defp get_current_replacement(%Analysis{} = analysis, %Range{} = range, segments) do | ||
with {:ok, patches} <- replace_full_module_on_line(analysis, range.start.line, segments), | ||
{:ok, edits} <- Ast.patches_to_edits(analysis.document, patches) do | ||
edits | ||
else | ||
_ -> | ||
[] | ||
end | ||
end | ||
|
||
defp replace_full_module_on_line(%Analysis{} = analysis, line, segments) do | ||
aliased_module = | ||
segments | ||
|> List.last() | ||
|> List.wrap() | ||
|> Module.concat() | ||
|> Formats.module() | ||
|
||
analysis.document | ||
|> Ast.traverse_line(line, [], fn | ||
%Zipper{node: {:__aliases__, _, ^segments}} = zipper, patches -> | ||
range = Sourceror.get_range(zipper.node) | ||
|
||
patch = %{range: range, change: aliased_module} | ||
{zipper, [patch | patches]} | ||
|
||
zipper, acc -> | ||
{zipper, acc} | ||
end) | ||
|> case do | ||
{:ok, _, patches} -> {:ok, patches} | ||
error -> error | ||
end | ||
end | ||
|
||
@similarity_threshold 0.75 | ||
defp similar?(a, b), do: String.jaro_distance(a, b) >= @similarity_threshold | ||
|
||
defp filter_by_resolution(modules_stream, {:call, _module, function, _arity}) do | ||
query_function = Atom.to_string(function) | ||
|
||
Stream.filter(modules_stream, fn module -> | ||
case Modules.fetch_functions(module) do | ||
{:ok, functions} -> | ||
Enum.any?(functions, fn {name, _arity} -> | ||
module_function = Atom.to_string(name) | ||
similar?(module_function, query_function) | ||
end) | ||
|
||
_ -> | ||
false | ||
end | ||
end) | ||
end | ||
|
||
defp filter_by_resolution(modules_stream, {:struct, _}) do | ||
Stream.filter(modules_stream, fn module -> | ||
case Modules.fetch_functions(module) do | ||
{:ok, functions} -> Keyword.has_key?(functions, :__struct__) | ||
_ -> false | ||
end | ||
end) | ||
end | ||
|
||
defp filter_by_resolution(modules_stream, _) do | ||
modules_stream | ||
end | ||
|
||
def possible_aliases(unaliased_module) do | ||
module_subject = Formats.module(unaliased_module) | ||
|
||
case Ast.Module.safe_split(unaliased_module) do | ||
{:elixir, unaliased_strings} -> | ||
module_subject | ||
|> do_fuzzy_search() | ||
|> Stream.filter(fn module -> | ||
{:elixir, split} = Ast.Module.safe_split(module) | ||
alias_as = List.last(split) | ||
subject_module = module | ||
RemoteControl.Module.Loader.ensure_loaded(subject_module) | ||
|
||
protocol_or_implementation? = function_exported?(module, :__impl__, 1) | ||
|
||
not protocol_or_implementation? and | ||
Enum.any?(unaliased_strings, &similar?(&1, alias_as)) | ||
end) | ||
|
||
_ -> | ||
[] | ||
end | ||
end | ||
|
||
defp do_fuzzy_search(subject) do | ||
# Note: we can't use the indexer's fuzzy matcher here, since it | ||
# ignores all deps, and then we won't be able to alias any deps module | ||
|
||
for {mod, _, _} <- all_modules(), | ||
elixir_module?(mod), | ||
not Namespace.Module.prefixed?(mod) do | ||
module_name = List.to_atom(mod) | ||
|
||
%Entry{ | ||
id: module_name, | ||
path: "", | ||
subject: module_name, | ||
subtype: :definition, | ||
type: :module | ||
} | ||
end | ||
|> Fuzzy.from_entries() | ||
|> Fuzzy.match(subject) | ||
end | ||
|
||
defp all_modules do | ||
# Note: this is for testing | ||
:code.all_available() | ||
end | ||
|
||
defp elixir_module?([?E, ?l, ?i, ?x, ?i, ?r, ?. | _]), do: true | ||
defp elixir_module?(_), do: false | ||
end |
Oops, something went wrong.