diff --git a/apps/language_server/lib/language_server.ex b/apps/language_server/lib/language_server.ex index f913823ef..88b674ef3 100644 --- a/apps/language_server/lib/language_server.ex +++ b/apps/language_server/lib/language_server.ex @@ -9,7 +9,7 @@ defmodule ElixirLS.LanguageServer do children = [ {ElixirLS.LanguageServer.Server, ElixirLS.LanguageServer.Server}, {ElixirLS.LanguageServer.JsonRpc, name: ElixirLS.LanguageServer.JsonRpc}, - {ElixirLS.LanguageServer.Providers.WorkspaceSymbols, []} + {ElixirLS.LanguageServer.Providers.WorkspaceSymbols, [args: [log?: true]]} ] opts = [strategy: :one_for_one, name: ElixirLS.LanguageServer.Supervisor, max_restarts: 0] diff --git a/apps/language_server/lib/language_server/protocol/symbol_information.ex b/apps/language_server/lib/language_server/protocol/symbol_information.ex index f6d3e18a9..ee1b1a624 100644 --- a/apps/language_server/lib/language_server/protocol/symbol_information.ex +++ b/apps/language_server/lib/language_server/protocol/symbol_information.ex @@ -6,4 +6,11 @@ defmodule ElixirLS.LanguageServer.Protocol.SymbolInformation do """ @derive JasonVendored.Encoder defstruct [:name, :kind, :location, :containerName] + + @type t :: %__MODULE__{ + name: String.t(), + kind: integer(), + location: map(), + containerName: any() + } end diff --git a/apps/language_server/lib/language_server/providers/workspace_symbols.ex b/apps/language_server/lib/language_server/providers/workspace_symbols.ex index 91a22ccf6..800888ebd 100644 --- a/apps/language_server/lib/language_server/providers/workspace_symbols.ex +++ b/apps/language_server/lib/language_server/providers/workspace_symbols.ex @@ -6,70 +6,32 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do """ use GenServer - alias ElixirLS.LanguageServer.ErlangSourceFile - alias ElixirLS.LanguageServer.SourceFile - alias ElixirLS.LanguageServer.Providers.SymbolUtils alias ElixirLS.LanguageServer.JsonRpc + alias ElixirLS.LanguageServer.Providers.DocumentSymbols + alias ElixirLS.LanguageServer.Protocol.SymbolInformation - @arity_suffix_regex ~r/\/\d+$/ - - @type position_t :: %{ - line: non_neg_integer, - character: non_neg_integer - } - - @type range_t :: %{ - start: position_t, - end: position_t - } - - @type location_t :: %{ - uri: String.t(), - range: range_t - } - - @type symbol_information_t :: %{ - kind: integer, - name: String.t(), - location: location_t - } - - # Location in annotations from OTP 23 and previous contain only line number. - # OTP 24 annotations may also be {line, column}, depending on compiler - # options. - @typep erl_location_t :: non_neg_integer | {non_neg_integer, non_neg_integer} - @typep key_t :: :modules | :functions | :types | :callbacks - @typep symbol_t :: module | {module, atom, non_neg_integer} - @typep state_t :: %{ - required(key_t) => [symbol_information_t], - modified_uris: [String.t()] - } - - @symbol_codes for {key, kind} <- [ - modules: :module, - functions: :function, - types: :class, - callbacks: :event - ], - into: %{}, - do: {key, SymbolUtils.symbol_kind_to_code(kind)} - - ## Client API - - @spec symbols(String.t()) :: {:ok, [symbol_information_t]} + @spec symbols(String.t(), module()) :: {:ok, [SymbolInformation.t()]} def symbols(query, server \\ __MODULE__) do results = query(query, server) {:ok, results} end - def start_link(opts) do - GenServer.start_link(__MODULE__, :ok, opts |> Keyword.put_new(:name, __MODULE__)) + defp query(query, server) do + query = String.trim(query) + + case query do + "" -> + GenServer.call(server, :all_symbols) + + query -> + GenServer.call(server, {:query, query}) + end end - def notify_build_complete(server \\ __MODULE__, override_test_mode \\ false) do + def set_paths(paths, server \\ __MODULE__, override_test_mode \\ false) do unless Application.get_env(:language_server, :test_mode) && not override_test_mode do - GenServer.cast(server, :build_complete) + GenServer.call(server, {:paths, paths}) end end @@ -80,453 +42,152 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do end end - ## Server Callbacks - - @impl GenServer - def init(:ok) do - {:ok, - %{ - modules: [], - modules_indexed: false, - types: [], - types_indexed: false, - callbacks: [], - callbacks_indexed: false, - functions: [], - functions_indexed: false, - indexing: false, - modified_uris: [] - }} - end - - @impl GenServer - def handle_call({:query, query}, from, state) do - {:ok, _pid} = - Task.start_link(fn -> - results = get_results(state, query) - GenServer.reply(from, results) - end) - - {:noreply, state} - end - - @impl GenServer - # not yet indexed - def handle_cast( - :build_complete, - state = %{ - indexing: false, - modules_indexed: false, - functions_indexed: false, - types_indexed: false, - callbacks_indexed: false - } - ) do - JsonRpc.log_message(:info, "[ElixirLS WorkspaceSymbols] Indexing...") - - module_paths = - :code.all_loaded() - |> process_chunked(fn chunk -> - for {module, beam_file} <- chunk, - path = find_module_path(module, beam_file), - path != nil, - do: {module, path} - end) - - JsonRpc.log_message(:info, "[ElixirLS WorkspaceSymbols] Module discovery complete") - - index(module_paths) - - {:noreply, %{state | indexing: true}} - end - - @impl GenServer - # indexed but some uris were modified - def handle_cast( - :build_complete, - %{ - indexing: false, - modified_uris: modified_uris = [_ | _] - } = state - ) do - JsonRpc.log_message(:info, "[ElixirLS WorkspaceSymbols] Updating index...") - - module_paths = - :code.all_loaded() - |> process_chunked(fn chunk -> - for {module, beam_file} <- chunk, - path = find_module_path(module, beam_file), - SourceFile.path_to_uri(path) in modified_uris, - do: {module, path} - end) - - JsonRpc.log_message( - :info, - "[ElixirLS WorkspaceSymbols] #{length(module_paths)} modules need reindexing" + def start_link(opts) do + GenServer.start_link( + __MODULE__, + Keyword.get(opts, :args, []), + Keyword.put_new(opts, :name, __MODULE__) ) - - index(module_paths) - - modules = - state.modules - |> Enum.reject(&(&1.location.uri in modified_uris)) - - functions = - state.functions - |> Enum.reject(&(&1.location.uri in modified_uris)) - - types = - state.types - |> Enum.reject(&(&1.location.uri in modified_uris)) - - callbacks = - state.callbacks - |> Enum.reject(&(&1.location.uri in modified_uris)) - - {:noreply, - %{ - state - | modules: modules, - modules_indexed: false, - functions: functions, - functions_indexed: false, - types: types, - types_indexed: false, - callbacks: callbacks, - callbacks_indexed: false, - indexing: true, - modified_uris: [] - }} - end - - # indexed and no uris momified or already indexing - def handle_cast(:build_complete, state) do - {:noreply, state} end - @impl GenServer - def handle_cast({:uris_modified, uris}, state) do - state = %{state | modified_uris: uris ++ state.modified_uris} - - {:noreply, state} - end + ## Server Callbacks @impl GenServer - def handle_info({:indexing_complete, key, results}, state) do - state = - state - |> Map.put(key, results ++ state[key]) - |> Map.put(:"#{key}_indexed", true) - - indexed = - state.modules_indexed and state.functions_indexed and state.types_indexed and - state.callbacks_indexed - - {:noreply, %{state | indexing: not indexed}} - end - - ## Helpers - - defp find_module_location(module, path) do - if String.ends_with?(path, ".erl") do - ErlangSourceFile.module_line(path) - else - SourceFile.module_line(module) - end - end - - defp find_function_location(module, function, arity, path) do - if String.ends_with?(path, ".erl") do - ErlangSourceFile.function_line(path, function) + def init(args) do + if Keyword.has_key?(args, :paths) do + {:ok, + %{ + symbols: Map.new(), + paths: Keyword.get(args, :paths), + log?: Keyword.get(args, :log?, false) + }, {:continue, :index}} else - SourceFile.function_line(module, function, arity) + {:ok, + %{ + symbols: Map.new(), + paths: Keyword.get(args, :paths), + log?: Keyword.get(args, :log?, false) + }} end end - defp find_module_path(module, beam_file) do - file = - with true <- Code.ensure_loaded?(module), - path when not is_nil(path) <- module.module_info(:compile)[:source], - path_binary = List.to_string(path), - true <- File.exists?(path_binary, [:raw]) do - path_binary - else - _ -> nil - end - - if file do - file - else - with beam_file when not is_nil(beam_file) <- - ErlangSourceFile.get_beam_file(module, beam_file), - erl_file = ErlangSourceFile.beam_file_to_erl_file(beam_file), - true <- File.exists?(erl_file, [:raw]) do - erl_file - else - _ -> nil - end - end - end - - defp get_score(item, query, query_downcase, query_length, arity_suffix) do - item_downcase = String.downcase(item) - - parts = item |> String.split(".") - - cond do - # searching for an erlang module but item is an Elixir module - String.starts_with?(query, ":") and not String.starts_with?(item, ":") -> - 0.0 - - # searching for an Elixir module but item is an erlang module - Regex.match?(~r/^[A-Z]/, query) and String.starts_with?(item, ":") -> - 0.0 - - # searching for an Elixir module or erlang/Elixir function but item has no `.` - String.contains?(query, ".") and not String.contains?(item, ".") -> - 0.0 - - # query specifies arity and item's arity does not match - arity_suffix != nil and not String.ends_with?(item, arity_suffix) -> - 0.0 - - length(parts) > 1 and Enum.at(parts, -1) |> exact_or_contains?(query, query_length) -> - 2.0 - - length(parts) > 1 and - Enum.at(parts, -1) - |> String.downcase() - |> exact_or_contains?(query_downcase, query_length) -> - 1.8 - - exact_or_contains?(item, query, query_length) -> - 1.3 - - exact_or_contains?(item_downcase, query_downcase, query_length) -> - 1.2 - - query_length >= 3 -> - String.jaro_distance(item_downcase, query_downcase) + @impl GenServer + def handle_continue(:index, state) do + symbols = index(state.paths, state) - true -> - 0.0 - end + {:noreply, %{state | symbols: symbols}} end - defp exact_or_contains?(string, needle = "/" <> _, needle_length) when needle_length < 3 do - String.ends_with?(string, needle) - end + defp index(paths, state) do + show(state, :log, "[ElixirLS WorkspaceSymbols] Indexing...") - defp exact_or_contains?(string, needle, needle_length) when needle_length < 3 do - string_no_arity = Regex.replace(@arity_suffix_regex, string, "") - string_no_arity == needle - end + root_paths = paths - defp exact_or_contains?(string, needle, _needle_length), do: String.contains?(string, needle) + paths = Enum.flat_map(root_paths, fn rp -> Path.wildcard("#{rp}/**/*.{ex,exs}") end) - defp limit_results(list) do - list - |> Enum.sort_by(&elem(&1, 1), &>=/2) - |> Enum.reduce_while({[], false}, fn {element, score}, {list, match_found} -> - match_found = match_found or score > 1.0 + symbols = + for file <- paths, into: %{} do + file = Path.absname(file) + uri = "file://#{file}" - if match_found do - if score > 1.0 do - {:cont, {[element | list], match_found}} - else - {:halt, {list, match_found}} - end - else - if length(list) < 15 do - {:cont, {[element | list], match_found}} + with {:ok, source_file_text} <- File.read(file), + {:ok, symbols} <- DocumentSymbols.symbols(uri, source_file_text, false) do + {uri, symbols} else - {:halt, {list, match_found}} + _ -> + {uri, []} end end - end) - |> elem(0) - end - defp query(query, server) do - case String.trim(query) do - "" -> - [] + show(state, :log, "[ElixirLS WorkspaceSymbols] Finished indexing!") - trimmed -> - GenServer.call(server, {:query, trimmed}) - end + symbols end - defp index(module_paths) do - chunked_module_paths = chunk_by_schedulers(module_paths) + @impl GenServer + def handle_call({:paths, paths}, _from, state) do + symbols = index(paths, state) - index_async(:modules, fn -> - chunked_module_paths - |> do_process_chunked(fn chunk -> - for {module, path} <- chunk do - location = find_module_location(module, path) - build_result(:modules, module, path, location) - end - end) - end) - - index_async(:functions, fn -> - chunked_module_paths - |> do_process_chunked(fn chunk -> - for {module, path} <- chunk, - Code.ensure_loaded?(module), - {function, arity} <- module.module_info(:exports) do - {function, arity} = SourceFile.strip_macro_prefix({function, arity}) - location = find_function_location(module, function, arity, path) - - build_result(:functions, {module, function, arity}, path, location) - end - end) - end) - - index_async(:types, fn -> - chunked_module_paths - |> do_process_chunked(fn chunk -> - for {module, path} <- chunk, - # TODO: Don't call into here directly - {kind, {type, type_ast, args}} <- - ElixirSense.Core.Normalized.Typespec.get_types(module), - kind in [:type, :opaque] do - location = - case type_ast do - {_, location, _, _} -> location - {_, location, _} -> location - end - - build_result(:types, {module, type, length(args)}, path, location) - end - end) - end) - - index_async(:callbacks, fn -> - chunked_module_paths - |> do_process_chunked(fn chunk -> - for {module, path} <- chunk, - function_exported?(module, :behaviour_info, 1), - # TODO: Don't call into here directly - {{callback, arity}, [{:type, location, _, _}]} <- - ElixirSense.Core.Normalized.Typespec.get_callbacks(module) do - {callback, arity} = SourceFile.strip_macro_prefix({callback, arity}) - - build_result(:callbacks, {module, callback, arity}, path, location) - end - end) - end) + {:reply, :ok, %{state | paths: paths, symbols: symbols}} end - defp index_async(key, fun) do - self = self() + def handle_call({:query, _query}, _from, %{paths: nil} = state) do + {:reply, [], state} + end + + def handle_call({:query, query}, from, state) do + log(state, :info, "[ElixirLS WorkspaceSymbols] Querying...") {:ok, _pid} = Task.start_link(fn -> - results = fun.() - - send(self, {:indexing_complete, key, results}) + results = + state.symbols + |> Map.values() + |> List.flatten() + |> Enum.map(fn %{name: name} = symbol -> + {String.jaro_distance(String.downcase(name), String.downcase(query)), symbol} + end) + |> Enum.filter(fn {score, _} -> score > 0.1 end) + |> Enum.sort_by(fn {score, _} -> score end, &>=/2) + |> Enum.map(fn {_, symbol} -> symbol end) - JsonRpc.log_message( - :info, - "[ElixirLS WorkspaceSymbols] #{length(results)} #{key} added to index" - ) + GenServer.reply(from, results) end) - :ok + {:noreply, state} end - @spec get_results(state_t, String.t()) :: [symbol_information_t] - defp get_results(state, query) do - query_downcase = String.downcase(query) - query_length = String.length(query) - arity_suffix = Regex.run(@arity_suffix_regex, query) - - (state.modules ++ state.functions ++ state.types ++ state.callbacks) - |> process_chunked(fn chunk -> - chunk - |> Enum.map(&{&1, get_score(&1.name, query, query_downcase, query_length, arity_suffix)}) - |> Enum.reject(fn {_item, score} -> score < 0.1 end) - end) - |> limit_results + def handle_call(:all_symbols, _from, %{paths: nil} = state) do + {:reply, [], state} end - defp chunk_by_schedulers(enumerable) do - chunk_size = - Enum.count(enumerable) - |> div(System.schedulers_online()) - |> max(1) - - enumerable - |> Enum.chunk_every(chunk_size) - end + def handle_call(:all_symbols, from, state) do + log(state, :info, "[ElixirLS WorkspaceSymbols] Empty query, returning all symbols!") - defp process_chunked(enumerable, fun) do - enumerable - |> chunk_by_schedulers - |> do_process_chunked(fun) - end + {:ok, _pid} = + Task.start_link(fn -> + results = + state.symbols + |> Map.values() + |> List.flatten() - defp do_process_chunked(chunked_enumerable, fun) do - chunked_enumerable - |> Enum.map(fn chunk when is_list(chunk) -> - Task.async(fn -> - fun.(chunk) + GenServer.reply(from, results) end) - end) - |> Task.yield_many(:infinity) - |> Enum.flat_map(fn {_task, {:ok, result}} when is_list(result) -> - result - end) - end - @spec build_result(key_t, symbol_t, String.t(), nil | erl_location_t) :: symbol_information_t - defp build_result(key, symbol, path, location) do - %{ - kind: @symbol_codes |> Map.fetch!(key), - name: symbol_name(key, symbol), - location: %{ - uri: SourceFile.path_to_uri(path), - range: build_range(location) - } - } + {:noreply, state} end - @spec symbol_name(key_t, symbol_t) :: String.t() - defp symbol_name(:modules, module) do - inspect(module) - end + @impl GenServer + def handle_cast({:uris_modified, uris}, state) do + show(state, :log, "[ElixirLS WorkspaceSymbols] Indexing...") - defp symbol_name(:functions, {module, function, arity}) do - "#{inspect(module)}.#{function}/#{arity}" - end + symbols = + for uri <- uris, into: state.symbols do + file = URI.parse(uri).path - defp symbol_name(:types, {module, type, arity}) do - "#{inspect(module)}.#{type}/#{arity}" - end + with {:ok, source_file_text} <- File.read(file), + {:ok, symbols} <- DocumentSymbols.symbols(uri, source_file_text, false) do + {uri, symbols} + else + _ -> + {uri, []} + end + end - defp symbol_name(:callbacks, {module, callback, arity}) do - "#{inspect(module)}.#{callback}/#{arity}" - end + show(state, :log, "[ElixirLS WorkspaceSymbols] Finished indexing!") - @spec build_range(nil | erl_location_t) :: range_t - defp build_range(nil) do - # we don't care about utf16 positions here as we send 0 - %{ - start: %{line: 0, character: 0}, - end: %{line: 1, character: 0} - } + {:noreply, %{state | symbols: symbols}} end - # it's not worth to present column info here - defp build_range({line, _column}), do: build_range(line) + defp log(state, type, message) do + if state.log? do + JsonRpc.log_message(type, message) + end + end - defp build_range(line) do - # we don't care about utf16 positions here as we send 0 - %{ - start: %{line: max(line - 1, 0), character: 0}, - end: %{line: line, character: 0} - } + defp show(state, type, message) do + if state.log? do + JsonRpc.show_message(type, message) + end end end diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index b66a2f4c7..0b7fff08d 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -110,6 +110,10 @@ defmodule ElixirLS.LanguageServer.Server do GenServer.call(server, {:suggest_contracts, uri}, :infinity) end + def paths(server \\ __MODULE__) do + GenServer.call(server, :paths) + end + defguardp is_initialized(server_instance_id) when not is_nil(server_instance_id) ## Server Callbacks @@ -120,6 +124,7 @@ defmodule ElixirLS.LanguageServer.Server do end @impl GenServer + def handle_call({:request_finished, id, result}, _from, state = %__MODULE__{}) do case result do {:error, type, msg} -> JsonRpc.respond_with_error(id, type, msg) @@ -235,10 +240,6 @@ defmodule ElixirLS.LanguageServer.Server do _ -> handle_build_result(:error, [Diagnostics.exception_to_diagnostic(reason)], state) end - if reason == :normal do - WorkspaceSymbols.notify_build_complete() - end - state = if state.needs_build?, do: trigger_build(state), else: state {:noreply, state} end @@ -325,7 +326,9 @@ defmodule ElixirLS.LanguageServer.Server do # close notification send before JsonRpc.log_message( :warning, - "Received textDocument/didOpen for file that is already open. Received uri: #{inspect(uri)}" + "Received textDocument/didOpen for file that is already open. Received uri: #{ + inspect(uri) + }" ) state @@ -990,6 +993,23 @@ defmodule ElixirLS.LanguageServer.Server do old_diagnostics, state.source_files ) + paths = + if Mix.Project.umbrella?() do + Mix.Project.apps_paths() + |> Enum.flat_map(fn {p, path} -> + Mix.Project.in_project(p, path, fn _mod -> + elixirc_paths = Keyword.fetch!(Mix.Project.config(), :elixirc_paths) + + Enum.map(elixirc_paths, fn elixirc_path -> + Path.join(path, elixirc_path) + end) + end) + end) + else + Keyword.fetch!(Mix.Project.config(), :elixirc_paths) + end + + WorkspaceSymbols.set_paths(paths) state end diff --git a/apps/language_server/test/providers/workspace_symbols_test.exs b/apps/language_server/test/providers/workspace_symbols_test.exs index dcf86a3af..ba8a5a785 100644 --- a/apps/language_server/test/providers/workspace_symbols_test.exs +++ b/apps/language_server/test/providers/workspace_symbols_test.exs @@ -6,41 +6,37 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbolsTest do alias ElixirLS.Utils.PacketCapture packet_capture = start_supervised!({PacketCapture, self()}) - {:ok, pid} = WorkspaceSymbols.start_link(name: nil) - Process.group_leader(pid, packet_capture) - - state = :sys.get_state(pid) + {:ok, pid} = + WorkspaceSymbols.start_link( + name: nil, + args: [paths: ["test/support/fixtures/workspace_symbols"]] + ) - fixture_uri = - ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.module_info(:compile)[:source] - |> List.to_string() - |> ElixirLS.LanguageServer.SourceFile.path_to_uri() - - :sys.replace_state(pid, fn _ -> - %{ - state - | modules_indexed: true, - functions_indexed: true, - types_indexed: true, - callbacks_indexed: true, - modified_uris: [fixture_uri] - } - end) - - WorkspaceSymbols.notify_build_complete(pid, true) + Process.group_leader(pid, packet_capture) wait_until_indexed(pid) {:ok, server: pid} end - test "empty query", %{server: server} do - assert {:ok, []} == WorkspaceSymbols.symbols("", server) + test "empty query returns all symbols", %{server: server} do + expected_symbols = [ + "def some_function(a)", + "defmacro some_macro(a)", + "@callback some_callback(integer)", + "@callback some_macrocallback(integer)", + "@type some_type", + "@type some_opaque_type", + "ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols" + ] - assert_receive %{ - "method" => "window/logMessage", - "params" => %{"message" => "[ElixirLS WorkspaceSymbols] Updating index..."} - } + assert {:ok, list} = WorkspaceSymbols.symbols("", server) + + assert length(list) == 7 + + for symbol <- list do + assert symbol.name in expected_symbols + end end test "returns modules", %{server: server} do @@ -50,11 +46,12 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbolsTest do Enum.find(list, &(&1.name == "ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols")) assert module.kind == 2 - assert module.location.uri |> String.ends_with?("test/support/fixtures/workspace_symbols.ex") + + assert String.ends_with?(module.location.uri, "test/support/fixtures/workspace_symbols/workspace_symbols.ex") assert module.location.range == %{ - end: %{character: 0, line: 1}, - start: %{character: 0, line: 0} + "end" => %{"character" => 0, "line" => 0}, + "start" => %{"character" => 0, "line" => 0} } assert WorkspaceSymbols.symbols("work", server) @@ -65,95 +62,73 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbolsTest do test "returns functions", %{server: server} do assert {:ok, list} = WorkspaceSymbols.symbols("ElixirLS.LanguageServer.Fixtures.", server) - assert some_function = - Enum.find( - list, - &(&1.name == "ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.some_function/1") - ) + assert some_function = Enum.find(list, &(&1.name == "def some_function(a)")) assert some_function.kind == 12 - assert some_function.location.uri - |> String.ends_with?("test/support/fixtures/workspace_symbols.ex") + assert String.ends_with?( + some_function.location.uri, + "test/support/fixtures/workspace_symbols/workspace_symbols.ex" + ) assert some_function.location.range == %{ - end: %{character: 0, line: 2}, - start: %{character: 0, line: 1} + "end" => %{"character" => 6, "line" => 1}, + "start" => %{"character" => 6, "line" => 1} } assert WorkspaceSymbols.symbols("fun", server) |> elem(1) - |> Enum.any?( - &(&1.name == "ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.some_function/1") - ) + |> Enum.any?(&(&1.name == "def some_function(a)")) end test "returns types", %{server: server} do assert {:ok, list} = WorkspaceSymbols.symbols("ElixirLS.LanguageServer.Fixtures.", server) - - assert some_type = - Enum.find( - list, - &(&1.name == "ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.some_type/0") - ) - + assert some_type = Enum.find(list, &(&1.name == "@type some_type")) assert some_type.kind == 5 - assert some_type.location.uri - |> String.ends_with?("test/support/fixtures/workspace_symbols.ex") + assert String.ends_with?( + some_type.location.uri, + "test/support/fixtures/workspace_symbols/workspace_symbols.ex" + ) assert some_type.location.range == %{ - end: %{character: 0, line: 8}, - start: %{character: 0, line: 7} + "end" => %{"character" => 3, "line" => 7}, + "start" => %{"character" => 3, "line" => 7} } - assert Enum.any?( - list, - &(&1.name == "ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.some_opaque_type/0") - ) + assert Enum.any?(list, &(&1.name == "@type some_opaque_type")) assert WorkspaceSymbols.symbols("opa", server) |> elem(1) - |> Enum.any?( - &(&1.name == "ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.some_opaque_type/0") - ) + |> Enum.any?(&(&1.name == "@type some_opaque_type")) end test "returns callbacks", %{server: server} do assert {:ok, list} = WorkspaceSymbols.symbols("ElixirLS.LanguageServer.Fixtures.", server) - - assert some_callback = - Enum.find( - list, - &(&1.name == "ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.some_callback/1") - ) - + assert some_callback = Enum.find(list, &(&1.name == "@callback some_callback(integer)")) assert some_callback.kind == 24 - assert some_callback.location.uri - |> String.ends_with?("test/support/fixtures/workspace_symbols.ex") + assert String.ends_with?( + some_callback.location.uri, + "test/support/fixtures/workspace_symbols/workspace_symbols.ex" + ) assert some_callback.location.range == %{ - end: %{character: 0, line: 5}, - start: %{character: 0, line: 4} + "end" => %{"character" => 3, "line" => 4}, + "start" => %{"character" => 3, "line" => 4} } - assert Enum.any?( - list, - &(&1.name == "ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.some_macrocallback/1") - ) + assert Enum.any?(list, &(&1.name == "@callback some_macrocallback(integer)")) assert WorkspaceSymbols.symbols("macr", server) |> elem(1) - |> Enum.any?( - &(&1.name == "ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.some_macrocallback/1") - ) + |> Enum.any?(&(&1.name == "@callback some_macrocallback(integer)")) end defp wait_until_indexed(pid) do state = :sys.get_state(pid) - if state.modules == [] or state.functions == [] or state.types == [] or state.callbacks == [] do + if Enum.empty?(state.symbols) do Process.sleep(500) wait_until_indexed(pid) end diff --git a/apps/language_server/test/support/fixtures/workspace_symbols.ex b/apps/language_server/test/support/fixtures/workspace_symbols/workspace_symbols.ex similarity index 100% rename from apps/language_server/test/support/fixtures/workspace_symbols.ex rename to apps/language_server/test/support/fixtures/workspace_symbols/workspace_symbols.ex