|  | 
|  | 1 | +defmodule ElixirLS.LanguageServer.Experimental.Provider.CodeAction.AddAlias do | 
|  | 2 | +  alias ElixirLS.LanguageServer.Experimental.CodeMod | 
|  | 3 | +  alias ElixirLS.LanguageServer.Experimental.CodeMod.Ast | 
|  | 4 | +  alias ElixirLS.LanguageServer.Experimental.SourceFile | 
|  | 5 | +  alias ElixirSense.Core.Metadata | 
|  | 6 | +  alias ElixirSense.Core.Parser | 
|  | 7 | +  alias ElixirSense.Core.State.Env | 
|  | 8 | +  alias LSP.Requests.CodeAction | 
|  | 9 | +  alias LSP.Types.CodeAction, as: CodeActionResult | 
|  | 10 | +  alias LSP.Types.Diagnostic | 
|  | 11 | +  alias LSP.Types.TextEdit | 
|  | 12 | +  alias LSP.Types.Workspace | 
|  | 13 | + | 
|  | 14 | +  @undefined_module_re ~r/(.*) is undefined \(module (.*) is not available or is yet to be defined\)/s | 
|  | 15 | +  @unknown_struct_re ~r/\(CompileError\) (.*).__struct__\/1 is undefined, cannot expand struct (.*). Make sure the struct name is correct./s | 
|  | 16 | + | 
|  | 17 | +  @spec apply(CodeAction.t()) :: [CodeActionResult.t()] | 
|  | 18 | +  def apply(%CodeAction{} = code_action) do | 
|  | 19 | +    source_file = code_action.source_file | 
|  | 20 | +    diagnostics = get_in(code_action, [:context, :diagnostics]) || [] | 
|  | 21 | + | 
|  | 22 | +    Enum.flat_map(diagnostics, fn %Diagnostic{} = diagnostic -> | 
|  | 23 | +      one_based_line = extract_start_line(diagnostic) | 
|  | 24 | + | 
|  | 25 | +      with {:ok, module_string} <- parse_message(diagnostic.message), | 
|  | 26 | +           true <- module_present?(source_file, one_based_line, module_string), | 
|  | 27 | +           {:ok, suggestions} <- create_suggestions(module_string, source_file, one_based_line), | 
|  | 28 | +           {:ok, replies} <- build_code_actions(source_file, one_based_line, suggestions) do | 
|  | 29 | +        replies | 
|  | 30 | +      else | 
|  | 31 | +        _ -> [] | 
|  | 32 | +      end | 
|  | 33 | +    end) | 
|  | 34 | +  end | 
|  | 35 | + | 
|  | 36 | +  defp extract_start_line(%Diagnostic{} = diagnostic) do | 
|  | 37 | +    diagnostic.range.start.line | 
|  | 38 | +  end | 
|  | 39 | + | 
|  | 40 | +  defp parse_message(message) do | 
|  | 41 | +    case Regex.scan(@undefined_module_re, message) do | 
|  | 42 | +      [[_message, _function, module]] -> | 
|  | 43 | +        {:ok, module} | 
|  | 44 | + | 
|  | 45 | +      _ -> | 
|  | 46 | +        case Regex.scan(@unknown_struct_re, message) do | 
|  | 47 | +          [[_message, module, module]] -> {:ok, module} | 
|  | 48 | +          _ -> :error | 
|  | 49 | +        end | 
|  | 50 | +    end | 
|  | 51 | +  end | 
|  | 52 | + | 
|  | 53 | +  defp module_present?(source_file, one_based_line, module_string) do | 
|  | 54 | +    module = module_to_alias_list(module_string) | 
|  | 55 | + | 
|  | 56 | +    with {:ok, line_text} <- SourceFile.fetch_text_at(source_file, one_based_line), | 
|  | 57 | +         {:ok, line_ast} <- Ast.from(line_text) do | 
|  | 58 | +      line_ast | 
|  | 59 | +      |> Macro.postwalk(false, fn | 
|  | 60 | +        {:., _fun_meta, [{:__aliases__, _aliases_meta, ^module} | _fun]} = ast, _acc -> | 
|  | 61 | +          {ast, true} | 
|  | 62 | + | 
|  | 63 | +        {:%, _struct_meta, [{:__aliases__, _aliases_meta, ^module} | _fields]} = ast, _acc -> | 
|  | 64 | +          {ast, true} | 
|  | 65 | + | 
|  | 66 | +        other_ast, acc -> | 
|  | 67 | +          {other_ast, acc} | 
|  | 68 | +      end) | 
|  | 69 | +      |> elem(1) | 
|  | 70 | +    end | 
|  | 71 | +  end | 
|  | 72 | + | 
|  | 73 | +  @max_suggestions 3 | 
|  | 74 | +  defp create_suggestions(module_string, source_file, one_based_line) do | 
|  | 75 | +    with {:ok, current_namespace} <- current_module_namespace(source_file, one_based_line) do | 
|  | 76 | +      suggestions = | 
|  | 77 | +        ElixirSense.all_modules() | 
|  | 78 | +        |> Enum.filter(&String.ends_with?(&1, "." <> module_string)) | 
|  | 79 | +        |> Enum.sort_by(&same_namespace?(&1, current_namespace)) | 
|  | 80 | +        |> Enum.take(@max_suggestions) | 
|  | 81 | +        |> Enum.map(&module_to_alias_list/1) | 
|  | 82 | + | 
|  | 83 | +      {:ok, suggestions} | 
|  | 84 | +    end | 
|  | 85 | +  end | 
|  | 86 | + | 
|  | 87 | +  defp same_namespace?(suggested_module_string, current_namespace) do | 
|  | 88 | +    suggested_module_namespace = | 
|  | 89 | +      suggested_module_string | 
|  | 90 | +      |> module_to_alias_list() | 
|  | 91 | +      |> List.first() | 
|  | 92 | +      |> Atom.to_string() | 
|  | 93 | + | 
|  | 94 | +    current_namespace == suggested_module_namespace | 
|  | 95 | +  end | 
|  | 96 | + | 
|  | 97 | +  defp current_module_namespace(source_file, one_based_line) do | 
|  | 98 | +    %Metadata{lines_to_env: lines_to_env} = | 
|  | 99 | +      source_file | 
|  | 100 | +      |> SourceFile.to_string() | 
|  | 101 | +      |> Parser.parse_string(true, true, one_based_line) | 
|  | 102 | + | 
|  | 103 | +    case Map.get(lines_to_env, one_based_line) do | 
|  | 104 | +      nil -> | 
|  | 105 | +        :error | 
|  | 106 | + | 
|  | 107 | +      %Env{module: module} -> | 
|  | 108 | +        namespace = | 
|  | 109 | +          module | 
|  | 110 | +          |> module_to_alias_list() | 
|  | 111 | +          |> List.first() | 
|  | 112 | +          |> Atom.to_string() | 
|  | 113 | + | 
|  | 114 | +        {:ok, namespace} | 
|  | 115 | +    end | 
|  | 116 | +  end | 
|  | 117 | + | 
|  | 118 | +  defp module_to_alias_list(module) when is_atom(module) do | 
|  | 119 | +    case Atom.to_string(module) do | 
|  | 120 | +      "Elixir." <> module_string -> module_to_alias_list(module_string) | 
|  | 121 | +      module_string -> module_to_alias_list(module_string) | 
|  | 122 | +    end | 
|  | 123 | +  end | 
|  | 124 | + | 
|  | 125 | +  defp module_to_alias_list(module) when is_binary(module) do | 
|  | 126 | +    module | 
|  | 127 | +    |> String.split(".") | 
|  | 128 | +    |> Enum.map(&String.to_atom/1) | 
|  | 129 | +  end | 
|  | 130 | + | 
|  | 131 | +  defp build_code_actions(source_file, one_based_line, suggestions) do | 
|  | 132 | +    with {:ok, edits_per_suggestion} <- | 
|  | 133 | +           text_edits_per_suggestion(source_file, one_based_line, suggestions) do | 
|  | 134 | +      case edits_per_suggestion do | 
|  | 135 | +        [] -> | 
|  | 136 | +          :error | 
|  | 137 | + | 
|  | 138 | +        [_ | _] -> | 
|  | 139 | +          replies = | 
|  | 140 | +            Enum.map(edits_per_suggestion, fn {text_edits, alias_line, suggestion} -> | 
|  | 141 | +              text_edits = Enum.map(text_edits, &update_line(&1, alias_line)) | 
|  | 142 | + | 
|  | 143 | +              CodeActionResult.new( | 
|  | 144 | +                title: construct_title(suggestion), | 
|  | 145 | +                kind: :quick_fix, | 
|  | 146 | +                edit: Workspace.Edit.new(changes: %{source_file.uri => text_edits}) | 
|  | 147 | +              ) | 
|  | 148 | +            end) | 
|  | 149 | + | 
|  | 150 | +          {:ok, replies} | 
|  | 151 | +      end | 
|  | 152 | +    end | 
|  | 153 | +  end | 
|  | 154 | + | 
|  | 155 | +  defp text_edits_per_suggestion(source_file, one_based_line, suggestions) do | 
|  | 156 | +    suggestions | 
|  | 157 | +    |> Enum.reduce_while([], fn suggestion, acc -> | 
|  | 158 | +      case CodeMod.AddAlias.text_edits(source_file, one_based_line, suggestion) do | 
|  | 159 | +        {:ok, [], _alias_line} -> {:cont, acc} | 
|  | 160 | +        {:ok, edits, alias_line} -> {:cont, [{edits, alias_line, suggestion} | acc]} | 
|  | 161 | +        :error -> {:halt, :error} | 
|  | 162 | +      end | 
|  | 163 | +    end) | 
|  | 164 | +    |> case do | 
|  | 165 | +      :error -> :error | 
|  | 166 | +      edits -> {:ok, edits} | 
|  | 167 | +    end | 
|  | 168 | +  end | 
|  | 169 | + | 
|  | 170 | +  defp update_line(%TextEdit{} = text_edit, line_number) do | 
|  | 171 | +    text_edit | 
|  | 172 | +    |> put_in([:range, :start, :line], line_number - 1) | 
|  | 173 | +    |> put_in([:range, :end, :line], line_number - 1) | 
|  | 174 | +  end | 
|  | 175 | + | 
|  | 176 | +  defp construct_title(suggestion) do | 
|  | 177 | +    module_string = Enum.map_join(suggestion, ".", &Atom.to_string/1) | 
|  | 178 | + | 
|  | 179 | +    "Add alias #{module_string}" | 
|  | 180 | +  end | 
|  | 181 | +end | 
0 commit comments