From e09f262f955e6843e062b40d74bda53d5cf055fb Mon Sep 17 00:00:00 2001 From: Mitchell Hanberg Date: Tue, 30 Apr 2024 17:22:36 -0400 Subject: [PATCH] fixup! refactor(completions): improve cursor position detection --- lib/next_ls.ex | 76 +++++++++++++++++------ lib/next_ls/helpers/ast_helpers.ex | 98 ++++++++++++------------------ mix.lock | 4 +- test/next_ls/completions_test.exs | 40 ++++++++++++ 4 files changed, 139 insertions(+), 79 deletions(-) diff --git a/lib/next_ls.ex b/lib/next_ls.ex index 8408ff41..2779445b 100644 --- a/lib/next_ls.ex +++ b/lib/next_ls.ex @@ -588,21 +588,43 @@ defmodule NextLS do source = Enum.join(document, "\n") - ast = - source - |> Spitfire.parse(literal_encoder: &{:ok, {:__block__, [{:literal, true} | &2], [&1]}}) - |> then(fn - {:ok, ast} -> ast - {:error, ast, _} -> ast - {:error, :no_fuel_remaining} -> nil - end) + {ms, ast} = + :timer.tc( + fn -> + source + |> Spitfire.parse() + |> then(fn + {:ok, ast} -> ast + {:error, ast, _} -> ast + {:error, :no_fuel_remaining} -> nil + end) + end, + :millisecond + ) + + Logger.debug("parsing: #{ms}ms") - with_cursor_zipper = - NextLS.ASTHelpers.find_cursor(ast, line: position.line + 1, column: position.character + 1) + {ms, with_cursor_zipper} = + :timer.tc( + fn -> + NextLS.ASTHelpers.find_cursor(ast, line: position.line + 1, column: position.character + 1) + end, + :millisecond + ) + + Logger.debug("find_cursor: #{ms}ms") # dbg(Sourceror.Zipper.node(with_cursor_zipper)) - env = NextLS.ASTHelpers.Env.build(with_cursor_zipper, %{line: position.line + 1, column: position.character + 1}) + {ms, env} = + :timer.tc( + fn -> + NextLS.ASTHelpers.Env.build(with_cursor_zipper, %{line: position.line + 1, column: position.character + 1}) + end, + :millisecond + ) + + Logger.debug("build env: #{ms}ms") document_slice = document @@ -621,8 +643,15 @@ defmodule NextLS do for {runtime, %{uri: wuri}} <- entries, String.starts_with?(uri, wuri) do ast = Sourceror.Zipper.node(with_cursor_zipper) - {:ok, {_, _, _, macro_env}} = - Runtime.expand(runtime, ast, Path.basename(uri)) + {ms, {:ok, {_, _, _, macro_env}}} = + :timer.tc( + fn -> + Runtime.expand(runtime, ast, Path.basename(uri)) + end, + :millisecond + ) + + Logger.debug("expand: #{ms}ms") env = env @@ -631,11 +660,22 @@ defmodule NextLS do |> Map.put(:aliases, macro_env.aliases) |> Map.put(:attrs, macro_env.attrs) - {wuri, - document_slice - |> String.to_charlist() - |> Enum.reverse() - |> NextLS.Autocomplete.expand(runtime, env)} + doc = + document_slice + |> String.to_charlist() + |> Enum.reverse() + + {ms, result} = + :timer.tc( + fn -> + NextLS.Autocomplete.expand(doc, runtime, env) + end, + :millisecond + ) + + Logger.debug("complete: #{ms}ms") + + {wuri, result} end case result do diff --git a/lib/next_ls/helpers/ast_helpers.ex b/lib/next_ls/helpers/ast_helpers.ex index f5d183c9..fbba5047 100644 --- a/lib/next_ls/helpers/ast_helpers.ex +++ b/lib/next_ls/helpers/ast_helpers.ex @@ -207,86 +207,66 @@ defmodule NextLS.ASTHelpers do def find_cursor(ast, position) do ast = {:__block__, [], [ast]} - {tree, id} = + {tree, cursor_tree} = ast |> Zipper.zip() - |> Zipper.traverse(0, fn tree, id -> + |> Zipper.traverse(nil, fn tree, acc -> node = Zipper.node(tree) node_range = Sourceror.Range.get_range(node) + is_literal = Macro.quoted_literal?(node) is_inside = - with nil <- node_range do + with nil <- node_range, is_literal do false else _ -> sourceror_inside?(node_range, position) end - tree = - case node do - {_, [{:literal, true} | _], [literal]} -> Zipper.replace(tree, literal) - _ -> tree - end - - if is_inside do - id = id + 1 + cond do + is_literal -> + {tree, acc} - tree = - Zipper.update(tree, fn node -> - Macro.update_meta(node, fn meta -> - Keyword.put(meta, :id, id) - end) - end) + is_inside -> + {tree, tree} - {tree, id} - else - {tree, id} + true -> + {tree, acc} end end) - # PERF: doing a second pass, can we improve? - Zipper.traverse(tree, fn tree -> - node = Zipper.node(tree) - - case node do - {_, [{:id, cid} | _], _} -> - tree = - Zipper.update(tree, fn node -> - Macro.update_meta(node, fn meta -> - Keyword.delete(meta, :id) - end) - end) - - tree = - if cid == id do - # insert our "cursor" node - left = Zipper.left(tree) - up = Zipper.up(tree) - - tree = - cond do - up && match?({:<-, _, _}, Zipper.node(up)) -> - up |> Zipper.insert_left({:__cursor__, [], []}) |> Zipper.down() |> Zipper.rightmost() + if cursor_tree do + left = Zipper.left(cursor_tree) + up = Zipper.up(cursor_tree) - left && Zipper.node(left) == :do -> - Zipper.update(tree, fn n -> - {:__block__, [], [n, {:__cursor__, [], []}]} - end) + cond_result = + cond do + up && match?({:"::", _, _}, Zipper.node(up)) -> + up + |> Zipper.up() + |> Zipper.update(fn n -> + {:__block__, [], + [ + {:__cursor__, [], []}, + n + ]} + end) - true -> - Zipper.insert_right(tree, {:__cursor__, [], []}) - end + up && match?({:<-, _, _}, Zipper.node(up)) -> + up |> Zipper.insert_left({:__cursor__, [], []}) |> Zipper.down() |> Zipper.rightmost() - tree - else - tree - end + left && Zipper.node(left) == :do -> + Zipper.update(cursor_tree, fn n -> + {:__block__, [], [n, {:__cursor__, [], []}]} + end) - tree + true -> + Zipper.insert_right(cursor_tree, {:__cursor__, [], []}) + end - _ -> - tree - end - end) + Zipper.top(cond_result) + else + tree + end end def top(nil, acc, _callback), do: acc diff --git a/mix.lock b/mix.lock index bd9d3256..9c981e51 100644 --- a/mix.lock +++ b/mix.lock @@ -46,8 +46,8 @@ "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "req": {:hex, :req, "0.4.0", "1c759054dd64ef1b1a0e475c2d2543250d18f08395d3174c371b7746984579ce", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.9", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "f53eadc32ebefd3e5d50390356ec3a59ed2b8513f7da8c6c3f2e14040e9fe989"}, "schematic": {:hex, :schematic, "0.2.1", "0b091df94146fd15a0a343d1bd179a6c5a58562527746dadd09477311698dbb1", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0b255d65921e38006138201cd4263fd8bb807d9dfc511074615cd264a571b3b1"}, - "sourceror": {:hex, :sourceror, "1.0.2", "c5e86fdc14881f797749d1fe5df017ca66727a8146e7ee3e736605a3df78f3e6", [:mix], [], "hexpm", "832335e87d0913658f129d58b2a7dc0490ddd4487b02de6d85bca0169ec2bd79"}, - "spitfire": {:git, "https://github.com/elixir-tools/spitfire.git", "f913c6025875c9d69b4d35f94cae3e70c7f6320e", []}, + "sourceror": {:hex, :sourceror, "1.0.3", "111711c147f4f1414c07a67b45ad0064a7a41569037355407eda635649507f1d", [:mix], [], "hexpm", "56c21ef146c00b51bc3bb78d1f047cb732d193256a7c4ba91eaf828d3ae826af"}, + "spitfire": {:git, "https://github.com/elixir-tools/spitfire.git", "8f991b87b0543b7bd6801855a1f1be4e3b4bebb4", []}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "styler": {:hex, :styler, "0.8.1", "f3c0f65023e4bfbf7e7aa752d128b8475fdabfd30f96ee7314b84480cc56e788", [:mix], [], "hexpm", "1aa48d3aa689a639289af3d8254d40e068e98c083d6e5e3d1a695e71a147b344"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, diff --git a/test/next_ls/completions_test.exs b/test/next_ls/completions_test.exs index 9bead370..bd1e9ae5 100644 --- a/test/next_ls/completions_test.exs +++ b/test/next_ls/completions_test.exs @@ -4,6 +4,8 @@ defmodule NextLS.CompletionsTest do import GenLSP.Test import NextLS.Support.Utils + @moduletag init_options: %{"experimental" => %{"completions" => %{"enable" => true}}} + defmacrop assert_match({:in, _, [left, right]}) do quote do assert Enum.any?(unquote(right), fn x -> @@ -347,6 +349,44 @@ defmodule NextLS.CompletionsTest do } in results end + test "inside interpolation in strings", %{client: client, foo: foo} do + uri = uri(foo) + + did_open(client, foo, ~S""" + defmodule Foo do + def run(thing) do + "./lib/#{t}" + :ok + end + end + """) + + request client, %{ + method: "textDocument/completion", + id: 2, + jsonrpc: "2.0", + params: %{ + textDocument: %{ + uri: uri + }, + position: %{ + line: 2, + character: 13 + } + } + } + + assert_result 2, results + + assert %{ + "data" => nil, + "documentation" => "", + "insertText" => "thing", + "kind" => 6, + "label" => "thing" + } in results + end + test "defmodule infer name", %{client: client, foo: foo} do uri = uri(foo)