|
| 1 | +defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.ManipulatePipes do |
| 2 | + @moduledoc """ |
| 3 | + This module implements a custom command for converting function calls |
| 4 | + to pipe operators and pipes to function calls. |
| 5 | +
|
| 6 | + Returns a formatted source fragment. |
| 7 | + """ |
| 8 | + import ElixirLS.LanguageServer.Protocol |
| 9 | + |
| 10 | + alias ElixirLS.LanguageServer.{JsonRpc, Server} |
| 11 | + |
| 12 | + alias __MODULE__.AST |
| 13 | + |
| 14 | + @behaviour ElixirLS.LanguageServer.Providers.ExecuteCommand |
| 15 | + |
| 16 | + @newlines ["\r\n", "\n", "\r"] |
| 17 | + |
| 18 | + @impl ElixirLS.LanguageServer.Providers.ExecuteCommand |
| 19 | + def execute( |
| 20 | + %{"uri" => uri, "cursor_line" => line, "cursor_column" => col, "operation" => operation}, |
| 21 | + state |
| 22 | + ) |
| 23 | + when is_integer(line) and is_integer(col) and is_binary(uri) and |
| 24 | + operation in ["to_pipe", "from_pipe"] do |
| 25 | + # line and col are assumed to be 0-indexed |
| 26 | + source_file = Server.get_source_file(state, uri) |
| 27 | + |
| 28 | + {:ok, %{edited_text: edited_text, edit_range: edit_range}} = |
| 29 | + case operation do |
| 30 | + "to_pipe" -> |
| 31 | + to_pipe_at_cursor(source_file.text, line, col) |
| 32 | + |
| 33 | + "from_pipe" -> |
| 34 | + from_pipe_at_cursor(source_file.text, line, col) |
| 35 | + end |
| 36 | + |
| 37 | + label = |
| 38 | + case operation do |
| 39 | + "to_pipe" -> "Convert function call to pipe operator" |
| 40 | + "from_pipe" -> "Convert pipe operator to function call" |
| 41 | + end |
| 42 | + |
| 43 | + edit_result = |
| 44 | + JsonRpc.send_request("workspace/applyEdit", %{ |
| 45 | + "label" => label, |
| 46 | + "edit" => %{ |
| 47 | + "changes" => %{ |
| 48 | + uri => [%{"range" => edit_range, "newText" => edited_text}] |
| 49 | + } |
| 50 | + } |
| 51 | + }) |
| 52 | + |
| 53 | + case edit_result do |
| 54 | + {:ok, %{"applied" => true}} -> |
| 55 | + {:ok, nil} |
| 56 | + |
| 57 | + other -> |
| 58 | + {:error, :server_error, |
| 59 | + "cannot insert spec, workspace/applyEdit returned #{inspect(other)}"} |
| 60 | + end |
| 61 | + end |
| 62 | + |
| 63 | + defp to_pipe_at_cursor(text, line, col) do |
| 64 | + result = |
| 65 | + ElixirSense.Core.Source.walk_text( |
| 66 | + text, |
| 67 | + %{walked_text: "", function_call: nil, range: nil}, |
| 68 | + fn current_char, remaining_text, current_line, current_col, acc -> |
| 69 | + if current_line - 1 == line and current_col - 1 == col do |
| 70 | + {:ok, function_call, call_range} = |
| 71 | + get_function_call(line, col, acc.walked_text, current_char, remaining_text) |
| 72 | + |
| 73 | + {remaining_text, |
| 74 | + %{ |
| 75 | + acc |
| 76 | + | walked_text: acc.walked_text <> current_char, |
| 77 | + function_call: function_call, |
| 78 | + range: call_range |
| 79 | + }} |
| 80 | + else |
| 81 | + {remaining_text, %{acc | walked_text: acc.walked_text <> current_char}} |
| 82 | + end |
| 83 | + end |
| 84 | + ) |
| 85 | + |
| 86 | + case result do |
| 87 | + %{function_call: nil} -> |
| 88 | + {:error, :function_call_not_found} |
| 89 | + |
| 90 | + %{function_call: function_call, range: range} -> |
| 91 | + piped_text = AST.to_pipe(function_call) |
| 92 | + |
| 93 | + {:ok, %{edited_text: piped_text, edit_range: range}} |
| 94 | + end |
| 95 | + end |
| 96 | + |
| 97 | + defp from_pipe_at_cursor(text, line, col) do |
| 98 | + result = |
| 99 | + ElixirSense.Core.Source.walk_text( |
| 100 | + text, |
| 101 | + %{walked_text: "", pipe_call: nil, range: nil}, |
| 102 | + fn current_char, remaining_text, current_line, current_col, acc -> |
| 103 | + if current_line - 1 == line and current_col - 1 == col do |
| 104 | + {:ok, pipe_call, call_range} = |
| 105 | + get_pipe_call(line, col, acc.walked_text, current_char, remaining_text) |
| 106 | + |
| 107 | + {remaining_text, |
| 108 | + %{ |
| 109 | + acc |
| 110 | + | walked_text: acc.walked_text <> current_char, |
| 111 | + pipe_call: pipe_call, |
| 112 | + range: call_range |
| 113 | + }} |
| 114 | + else |
| 115 | + {remaining_text, %{acc | walked_text: acc.walked_text <> current_char}} |
| 116 | + end |
| 117 | + end |
| 118 | + ) |
| 119 | + |
| 120 | + case result do |
| 121 | + %{pipe_call: nil} -> |
| 122 | + {:error, :pipe_not_found} |
| 123 | + |
| 124 | + %{pipe_call: pipe_call, range: range} -> |
| 125 | + unpiped_text = AST.from_pipe(pipe_call) |
| 126 | + |
| 127 | + {:ok, %{edited_text: unpiped_text, edit_range: range}} |
| 128 | + end |
| 129 | + end |
| 130 | + |
| 131 | + defp get_function_call(line, col, head, current, original_tail) do |
| 132 | + tail = do_get_function_call(original_tail, "(", ")") |
| 133 | + |
| 134 | + {end_line, end_col} = |
| 135 | + if String.contains?(tail, @newlines) do |
| 136 | + tail_list = String.split(tail, @newlines) |
| 137 | + end_line = line + length(tail_list) - 1 |
| 138 | + end_col = tail_list |> Enum.at(-1) |> String.length() |
| 139 | + {end_line, end_col} |
| 140 | + else |
| 141 | + {line, col + String.length(tail) + 1} |
| 142 | + end |
| 143 | + |
| 144 | + text = head <> current <> tail |
| 145 | + |
| 146 | + call = get_function_call_before(text) |
| 147 | + |
| 148 | + {head, _tail} = String.split_at(call, -String.length(tail)) |
| 149 | + |
| 150 | + col = if head == "", do: col + 2, else: col - String.length(head) + 1 |
| 151 | + |
| 152 | + {:ok, call, range(line, col, end_line, end_col)} |
| 153 | + end |
| 154 | + |
| 155 | + defp do_get_function_call(text, start_char, end_char) do |
| 156 | + text |
| 157 | + |> do_get_function_call(start_char, end_char, %{paren_count: 0, text: ""}) |
| 158 | + |> Map.get(:text) |
| 159 | + |> IO.iodata_to_binary() |
| 160 | + end |
| 161 | + |
| 162 | + defp do_get_function_call(<<c::binary-size(1), tail::bitstring>>, start_char, end_char, acc) |
| 163 | + when c == start_char do |
| 164 | + do_get_function_call(tail, start_char, end_char, %{ |
| 165 | + acc |
| 166 | + | paren_count: acc.paren_count + 1, |
| 167 | + text: [acc.text | [c]] |
| 168 | + }) |
| 169 | + end |
| 170 | + |
| 171 | + defp do_get_function_call(<<c::binary-size(1), tail::bitstring>>, start_char, end_char, acc) |
| 172 | + when c == end_char do |
| 173 | + acc = %{acc | paren_count: acc.paren_count - 1, text: [acc.text | [c]]} |
| 174 | + |
| 175 | + if acc.paren_count <= 0 do |
| 176 | + acc |
| 177 | + else |
| 178 | + do_get_function_call(tail, start_char, end_char, acc) |
| 179 | + end |
| 180 | + end |
| 181 | + |
| 182 | + defp do_get_function_call(<<c::binary-size(1), tail::bitstring>>, start_char, end_char, acc) do |
| 183 | + do_get_function_call(tail, start_char, end_char, %{acc | text: [acc.text | [c]]}) |
| 184 | + end |
| 185 | + |
| 186 | + defp get_pipe_call(line, col, head, current, tail) do |
| 187 | + pipe_right = do_get_function_call(tail, "(", ")") |
| 188 | + |
| 189 | + pipe_left = |
| 190 | + head |
| 191 | + |> String.reverse() |
| 192 | + |> :unicode.characters_to_binary(:utf8, :utf16) |
| 193 | + |> do_get_pipe_call() |
| 194 | + |> :unicode.characters_to_binary(:utf16, :utf8) |
| 195 | + |
| 196 | + pipe_left = |
| 197 | + if String.contains?(pipe_left, ")") do |
| 198 | + get_function_call_before(head) |
| 199 | + else |
| 200 | + pipe_left |
| 201 | + end |
| 202 | + |
| 203 | + pipe_left = String.trim_leading(pipe_left) |
| 204 | + |
| 205 | + pipe_call = pipe_left <> current <> pipe_right |
| 206 | + |
| 207 | + {line_offset, tail_length} = |
| 208 | + pipe_left |
| 209 | + |> String.reverse() |
| 210 | + |> count_newlines_and_get_tail() |
| 211 | + |
| 212 | + start_line = line - line_offset |
| 213 | + |
| 214 | + start_col = |
| 215 | + if line_offset != 0 do |
| 216 | + head |
| 217 | + |> String.trim_trailing(pipe_left) |
| 218 | + |> String.split(["\r\n", "\n", "\r"]) |
| 219 | + |> Enum.at(-1, "") |
| 220 | + |> String.length() |
| 221 | + else |
| 222 | + col - tail_length |
| 223 | + end |
| 224 | + |
| 225 | + {line_offset, tail_length} = (current <> pipe_right) |> count_newlines_and_get_tail() |
| 226 | + |
| 227 | + end_line = line + line_offset |
| 228 | + |
| 229 | + end_col = |
| 230 | + if line_offset != 0 do |
| 231 | + tail_length |
| 232 | + else |
| 233 | + col + tail_length |
| 234 | + end |
| 235 | + |
| 236 | + {:ok, pipe_call, range(start_line, start_col, end_line, end_col)} |
| 237 | + end |
| 238 | + |
| 239 | + # do_get_pipe_call(text :: utf16 binary, {utf16 binary, has_passed_through_whitespace, should_halt}) |
| 240 | + defp do_get_pipe_call(text, acc \\ {"", false, false}) |
| 241 | + |
| 242 | + defp do_get_pipe_call(_text, {acc, _, true}), do: acc |
| 243 | + defp do_get_pipe_call("", {acc, _, _}), do: acc |
| 244 | + |
| 245 | + defp do_get_pipe_call(<<?\r::utf16, ?\n::utf16, _::bitstring>>, {acc, true, _}), |
| 246 | + do: <<?\r::utf16, ?\n::utf16, acc::bitstring>> |
| 247 | + |
| 248 | + defp do_get_pipe_call(<<0, c::utf8, _::bitstring>>, {acc, true, _}) |
| 249 | + when c in [?\t, ?\v, ?\r, ?\n, ?\s], |
| 250 | + do: <<c::utf16, acc::bitstring>> |
| 251 | + |
| 252 | + defp do_get_pipe_call(<<0, ?\r, 0, ?\n, text::bitstring>>, {acc, false, _}), |
| 253 | + do: do_get_pipe_call(text, {<<?\r::utf16, ?\n::utf16, acc::bitstring>>, false, false}) |
| 254 | + |
| 255 | + defp do_get_pipe_call(<<0, c::utf8, text::bitstring>>, {acc, false, _}) |
| 256 | + when c in [?\t, ?\v, ?\n, ?\s], |
| 257 | + do: do_get_pipe_call(text, {<<c::utf16, acc::bitstring>>, false, false}) |
| 258 | + |
| 259 | + defp do_get_pipe_call(<<0, c::utf8, text::bitstring>>, {acc, _, _}) |
| 260 | + when c in [?|, ?>], |
| 261 | + do: do_get_pipe_call(text, {<<c::utf16, acc::bitstring>>, false, false}) |
| 262 | + |
| 263 | + defp do_get_pipe_call(<<c::utf16, text::bitstring>>, {acc, _, _}), |
| 264 | + do: do_get_pipe_call(text, {<<c::utf16, acc::bitstring>>, true, false}) |
| 265 | + |
| 266 | + defp get_function_call_before(head) do |
| 267 | + call_without_function_name = |
| 268 | + head |
| 269 | + |> String.reverse() |
| 270 | + |> do_get_function_call(")", "(") |
| 271 | + |> String.reverse() |
| 272 | + |
| 273 | + function_name = |
| 274 | + head |
| 275 | + |> String.trim_trailing(call_without_function_name) |
| 276 | + |> get_function_name_from_tail() |
| 277 | + |
| 278 | + function_name <> call_without_function_name |
| 279 | + end |
| 280 | + |
| 281 | + defp get_function_name_from_tail(s) do |
| 282 | + s |
| 283 | + |> String.reverse() |
| 284 | + |> String.graphemes() |
| 285 | + |> Enum.reduce_while([], fn c, acc -> |
| 286 | + if String.match?(c, ~r/\s/) do |
| 287 | + {:halt, acc} |
| 288 | + else |
| 289 | + {:cont, [c | acc]} |
| 290 | + end |
| 291 | + end) |
| 292 | + |> IO.iodata_to_binary() |
| 293 | + end |
| 294 | + |
| 295 | + defp count_newlines_and_get_tail(s, acc \\ {0, 0}) |
| 296 | + |
| 297 | + defp count_newlines_and_get_tail("", acc), do: acc |
| 298 | + |
| 299 | + defp count_newlines_and_get_tail(s, {line_count, tail_length}) do |
| 300 | + case String.next_grapheme(s) do |
| 301 | + {g, tail} when g in ["\r\n", "\r", "\n"] -> |
| 302 | + count_newlines_and_get_tail(tail, {line_count + 1, 0}) |
| 303 | + |
| 304 | + {_, tail} -> |
| 305 | + count_newlines_and_get_tail(tail, {line_count, tail_length + 1}) |
| 306 | + end |
| 307 | + end |
| 308 | +end |
0 commit comments