From 1eb0d50ea7960448ac45ba6366a2edf664fb28c1 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 30 Jan 2020 01:27:45 +0100 Subject: [PATCH 01/13] add workspaceSymbol capability --- apps/language_server/lib/language_server.ex | 3 +- .../lib/language_server/erlang_source_file.ex | 46 ++ .../lib/language_server/protocol.ex | 8 + .../providers/document_symbols.ex | 31 +- .../language_server/providers/symbol_utils.ex | 34 ++ .../providers/workspace_symbols.ex | 468 ++++++++++++++++++ .../lib/language_server/server.ex | 19 + .../lib/language_server/source_file.ex | 13 +- .../test/providers/workspace_symbols_test.ex | 228 +++++++++ .../support/fixtures/workspace_symbols.ex | 10 + 10 files changed, 829 insertions(+), 31 deletions(-) create mode 100644 apps/language_server/lib/language_server/erlang_source_file.ex create mode 100644 apps/language_server/lib/language_server/providers/symbol_utils.ex create mode 100644 apps/language_server/lib/language_server/providers/workspace_symbols.ex create mode 100644 apps/language_server/test/providers/workspace_symbols_test.ex create mode 100644 apps/language_server/test/support/fixtures/workspace_symbols.ex diff --git a/apps/language_server/lib/language_server.ex b/apps/language_server/lib/language_server.ex index 9f7ad1f30..346688bc6 100644 --- a/apps/language_server/lib/language_server.ex +++ b/apps/language_server/lib/language_server.ex @@ -10,7 +10,8 @@ defmodule ElixirLS.LanguageServer do children = [ worker(ElixirLS.LanguageServer.Server, [ElixirLS.LanguageServer.Server]), - worker(ElixirLS.LanguageServer.JsonRpc, [[name: ElixirLS.LanguageServer.JsonRpc]]) + worker(ElixirLS.LanguageServer.JsonRpc, [[name: ElixirLS.LanguageServer.JsonRpc]]), + worker(ElixirLS.LanguageServer.Providers.WorkspaceSymbols, [[]]) ] opts = [strategy: :one_for_one, name: ElixirLS.LanguageServer.Supervisor, max_restarts: 0] diff --git a/apps/language_server/lib/language_server/erlang_source_file.ex b/apps/language_server/lib/language_server/erlang_source_file.ex new file mode 100644 index 000000000..9ce576a1d --- /dev/null +++ b/apps/language_server/lib/language_server/erlang_source_file.ex @@ -0,0 +1,46 @@ +defmodule ElixirLS.LanguageServer.ErlangSourceFile do + def get_beam_file(module, :preloaded) do + case :code.get_object_code(module) do + {_module, _binary, beam_file} -> beam_file + :error -> nil + end + end + + def get_beam_file(_module, beam_file), do: beam_file + + def beam_file_to_erl_file(beam_file) do + beam_file + |> to_string + |> String.replace( + Regex.recompile!(~r/(.+)\/ebin\/([^\s]+)\.beam$/), + "\\1/src/\\2.erl" + ) + end + + def module_line(file) do + find_line_by_regex(file, Regex.recompile!(~r/^-module/)) + end + + def function_line(file, function) do + # TODO use arity? + escaped = + function + |> Atom.to_string() + |> Regex.escape() + + find_line_by_regex(file, Regex.recompile!(~r/^#{escaped}\b/)) + end + + defp find_line_by_regex(file, regex) do + index = + file + |> File.read!() + |> String.split(["\n", "\r\n"]) + |> Enum.find_index(&String.match?(&1, regex)) + + case index do + nil -> nil + i -> i + 1 + end + end +end diff --git a/apps/language_server/lib/language_server/protocol.ex b/apps/language_server/lib/language_server/protocol.ex index 90af8f5c4..f6a6853b5 100644 --- a/apps/language_server/lib/language_server/protocol.ex +++ b/apps/language_server/lib/language_server/protocol.ex @@ -127,6 +127,14 @@ defmodule ElixirLS.LanguageServer.Protocol do end end + defmacro workspace_symbol_req(id, query) do + quote do + request(unquote(id), "workspace/symbol", %{ + "query" => unquote(query) + }) + end + end + defmacro signature_help_req(id, uri, line, character) do quote do request(unquote(id), "textDocument/signatureHelp", %{ diff --git a/apps/language_server/lib/language_server/providers/document_symbols.ex b/apps/language_server/lib/language_server/providers/document_symbols.ex index 02d2448e9..d4f06055d 100644 --- a/apps/language_server/lib/language_server/providers/document_symbols.ex +++ b/apps/language_server/lib/language_server/providers/document_symbols.ex @@ -5,34 +5,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do https://microsoft.github.io//language-server-protocol/specifications/specification-3-14/#textDocument_documentSymbol """ - @symbol_enum %{ - file: 1, - module: 2, - namespace: 3, - package: 4, - class: 5, - method: 6, - property: 7, - field: 8, - constructor: 9, - enum: 10, - interface: 11, - function: 12, - variable: 13, - constant: 14, - string: 15, - number: 16, - boolean: 17, - array: 18, - object: 19, - key: 20, - null: 21, - enum_member: 22, - struct: 23, - event: 24, - operator: 25, - type_parameter: 26 - } + alias ElixirLS.LanguageServer.Providers.SymbolUtils @defs [:def, :defp, :defmacro, :defmacrop, :defguard, :defguardp, :defdelegate] @@ -261,7 +234,7 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do defp build_symbol_information(uri, info) do %{ name: info.name, - kind: @symbol_enum[info.type], + kind: SymbolUtils.symbol_kind_to_code(info.type), range: location_to_range(info.location), selectionRange: location_to_range(info.location), children: build_symbol_information(uri, info.children) diff --git a/apps/language_server/lib/language_server/providers/symbol_utils.ex b/apps/language_server/lib/language_server/providers/symbol_utils.ex new file mode 100644 index 000000000..dec12df2a --- /dev/null +++ b/apps/language_server/lib/language_server/providers/symbol_utils.ex @@ -0,0 +1,34 @@ +defmodule ElixirLS.LanguageServer.Providers.SymbolUtils do + @symbol_enum %{ + file: 1, + module: 2, + namespace: 3, + package: 4, + class: 5, + method: 6, + property: 7, + field: 8, + constructor: 9, + enum: 10, + interface: 11, + function: 12, + variable: 13, + constant: 14, + string: 15, + number: 16, + boolean: 17, + array: 18, + object: 19, + key: 20, + null: 21, + enum_member: 22, + struct: 23, + event: 24, + operator: 25, + type_parameter: 26 + } + + for {kind, code} <- @symbol_enum do + def symbol_kind_to_code(unquote(kind)), do: unquote(code) + end +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 new file mode 100644 index 000000000..a09fb7332 --- /dev/null +++ b/apps/language_server/lib/language_server/providers/workspace_symbols.ex @@ -0,0 +1,468 @@ +defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do + @moduledoc """ + Workspace Symbols provider. Generates and returns `SymbolInfo[]`. + + https://microsoft.github.io//language-server-protocol/specifications/specification-3-14/#workspace_symbol + """ + use GenServer + + alias ElixirLS.LanguageServer.ErlangSourceFile + alias ElixirLS.LanguageServer.SourceFile + alias ElixirLS.LanguageServer.Providers.SymbolUtils + alias ElixirLS.LanguageServer.JsonRpc + + @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 + } + + @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]} + def symbols(query) do + results = + case query do + "f " <> fun_query -> + query(:functions, fun_query) + + "t " <> type_query -> + query(:types, type_query) + + "c " <> callback_query -> + query(:callbacks, callback_query) + + module_query -> + query(:modules, module_query) + end + + {:ok, results} + end + + def start_link(opts) do + GenServer.start_link(__MODULE__, :ok, opts |> Keyword.put(:name, __MODULE__)) + end + + def notify_build_complete do + GenServer.cast(__MODULE__, :build_complete) + end + + @spec notify_uris_modified([String.t()]) :: :ok + def notify_uris_modified(uris) do + GenServer.cast(__MODULE__, {:uris_modified, uris}) + end + + ## Server Callbacks + + @impl GenServer + def init(:ok) do + {:ok, + %{ + modules: [], + types: [], + callbacks: [], + functions: [], + modified_uris: [] + }} + end + + @impl GenServer + def handle_call({:query, key, query}, from, state) do + Task.start_link(fn -> + results = get_results(state, key, query) + GenServer.reply(from, results) + end) + + {:noreply, state} + end + + @impl GenServer + def handle_cast(:build_complete, %{modified_uris: []} = state) do + JsonRpc.log_message(:info, "[ElixirLS WorkspaceSymbols] Indexing...") + + module_paths = + :code.all_loaded() + |> chunk_by_schedulers(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} + end + + @impl GenServer + def handle_cast(:build_complete, %{modified_uris: modified_uris} = state) do + JsonRpc.log_message(:info, "[ElixirLS WorkspaceSymbols] Updating index...") + + module_paths = + :code.all_loaded() + |> chunk_by_schedulers(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" + ) + + 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, + functions: functions, + types: types, + callbacks: callbacks, + modified_uris: [] + }} + end + + @impl GenServer + def handle_cast({:uris_modified, uris}, state) do + state = + if state.modules == [] or state.types == [] or state.callbacks == [] or + state.functions == [] do + state + else + %{state | modified_uris: uris ++ state.modified_uris} + end + + {:noreply, state} + end + + @impl GenServer + def handle_info({:results, key, results}, state) do + {:noreply, state |> Map.put(key, results ++ state[key])} + end + + ## Helpers + + defp find_module_line(module, path) do + if String.ends_with?(path, ".erl") do + ErlangSourceFile.module_line(path) + else + SourceFile.module_line(module) + end + end + + defp find_function_line(module, function, arity, path) do + if String.ends_with?(path, ".erl") do + ErlangSourceFile.function_line(path, function) + else + SourceFile.function_line(module, function, arity) + 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) 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) do + erl_file + else + _ -> nil + end + end + end + + defp get_score(item, query) do + item_downcase = String.downcase(item) + query_downcase = String.downcase(query) + + parts = item |> String.split(".") + arity_suffix = Regex.run(~r/\/\d+$/, query) + + 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) |> String.contains?(query) -> + 2.0 + + length(parts) > 1 and + Enum.at(parts, -1) |> String.downcase() |> String.contains?(query_downcase) -> + 1.8 + + String.contains?(item, query) -> + 1.3 + + String.contains?(item_downcase, query_downcase) -> + 1.2 + + true -> + String.jaro_distance(item_downcase, query_downcase) + end + 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 + + 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}} + else + {:halt, {list, match_found}} + end + end + end) + |> elem(0) + end + + defp query(kind, query) do + case String.trim(query) do + "" -> + [] + + trimmed -> + GenServer.call(__MODULE__, {:query, kind, trimmed}) + end + end + + defp index(module_paths) do + index_async(:modules, fn -> + module_paths + |> chunk_by_schedulers(fn chunk -> + for {module, path} <- chunk do + line = find_module_line(module, path) + build_result(:modules, module, path, line) + end + end) + end) + + index_async(:functions, fn -> + module_paths + |> chunk_by_schedulers(fn chunk -> + for {module, path} <- chunk, + {function, arity} <- module.module_info(:exports) do + {function, arity} = strip_macro_prefix({function, arity}) + line = find_function_line(module, function, arity, path) + + build_result(:functions, {module, function, arity}, path, line) + end + end) + end) + + index_async(:types, fn -> + module_paths + |> chunk_by_schedulers(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 + line = + case type_ast do + {_, line, _, _} -> line + {_, line, _} -> line + end + + build_result(:types, {module, type, length(args)}, path, line) + end + end) + end) + + index_async(:callbacks, fn -> + module_paths + |> chunk_by_schedulers(fn chunk -> + for {module, path} <- chunk, + function_exported?(module, :behaviour_info, 1), + # TODO: Don't call into here directly + {{callback, arity}, [{:type, line, _, _}]} <- + ElixirSense.Core.Normalized.Typespec.get_callbacks(module) do + {callback, arity} = strip_macro_prefix({callback, arity}) + + build_result(:callbacks, {module, callback, arity}, path, line) + end + end) + end) + end + + defp index_async(key, fun) do + self = self() + + Task.start_link(fn -> + results = fun.() + + send(self, {:results, key, results}) + + JsonRpc.log_message( + :info, + "[ElixirLS WorkspaceSymbols] #{length(results)} #{key} added to index" + ) + end) + end + + @spec get_results(state_t, key_t, String.t()) :: [symbol_information_t] + defp get_results(state, key, query) do + state + |> Map.fetch!(key) + |> chunk_by_schedulers(fn chunk -> + chunk + |> Enum.map(&{&1, get_score(&1.name, query)}) + |> Enum.reject(fn {_item, score} -> score < 0.1 end) + end) + |> limit_results + end + + defp chunk_by_schedulers(enumerable, fun) do + chunk_size = + Enum.count(enumerable) + |> div(System.schedulers_online()) + |> max(1) + + enumerable + |> Enum.chunk_every(chunk_size) + |> Enum.map(fn chunk when is_list(chunk) -> + Task.async(fn -> + fun.(chunk) + 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 | non_neg_integer) :: symbol_information_t + defp build_result(key, symbol, path, line) do + %{ + kind: @symbol_codes |> Map.fetch!(key), + name: symbol_name(key, symbol), + location: %{ + uri: SourceFile.path_to_uri(path), + range: build_range(line) + } + } + end + + @spec symbol_name(key_t, symbol_t) :: String.t() + defp symbol_name(:modules, module) do + inspect(module) + end + + defp symbol_name(:functions, {module, function, arity}) do + "f #{inspect(module)}.#{function}/#{arity}" + end + + defp symbol_name(:types, {module, type, arity}) do + "t #{inspect(module)}.#{type}/#{arity}" + end + + defp symbol_name(:callbacks, {module, callback, arity}) do + "c #{inspect(module)}.#{callback}/#{arity}" + end + + @spec build_range(nil | non_neg_integer) :: range_t + defp build_range(nil) do + %{ + start: %{line: 0, character: 0}, + end: %{line: 1, character: 0} + } + end + + defp build_range(line) do + %{ + start: %{line: max(line - 1, 0), character: 0}, + end: %{line: line, character: 0} + } + end + + defp strip_macro_prefix({function, arity}) do + case Atom.to_string(function) do + "MACRO-" <> rest -> {String.to_atom(rest), arity - 1} + _other -> {function, arity} + end + end +end diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index 05b5418d7..74de0d062 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -26,6 +26,7 @@ defmodule ElixirLS.LanguageServer.Server do Formatting, SignatureHelp, DocumentSymbols, + WorkspaceSymbols, OnTypeFormatting, CodeLens, ExecuteCommand @@ -291,6 +292,7 @@ defmodule ElixirLS.LanguageServer.Server do end defp handle_notification(did_save(uri), state) do + WorkspaceSymbols.notify_uris_modified([uri]) state = update_in(state.source_files[uri], &%{&1 | dirty?: false}) trigger_build(state) end @@ -302,6 +304,11 @@ defmodule ElixirLS.LanguageServer.Server do (type in [1, 3] or not Map.has_key?(state.source_files, uri)) end) + changes + |> Enum.map(& &1["uri"]) + |> Enum.uniq() + |> WorkspaceSymbols.notify_uris_modified() + if needs_build, do: trigger_build(state), else: state end @@ -387,6 +394,15 @@ defmodule ElixirLS.LanguageServer.Server do {:async, fun, state} end + defp handle_request(workspace_symbol_req(_id, query), state) do + fun = fn -> + state.source_files + WorkspaceSymbols.symbols(query) + end + + {:async, fun, state} + end + defp handle_request(completion_req(_id, uri, line, character), state) do snippets_supported = !!get_in(state.client_capabilities, [ @@ -467,6 +483,7 @@ defmodule ElixirLS.LanguageServer.Server do "documentFormattingProvider" => Formatting.supported?(), "signatureHelpProvider" => %{"triggerCharacters" => ["("]}, "documentSymbolProvider" => true, + "workspaceSymbolProvider" => true, "documentOnTypeFormattingProvider" => %{"firstTriggerCharacter" => "\n"}, "codeLensProvider" => %{"resolveProvider" => false}, "executeCommandProvider" => %{"commands" => ["spec"]}, @@ -572,6 +589,8 @@ defmodule ElixirLS.LanguageServer.Server do GenServer.reply(from, contracts) end + WorkspaceSymbols.notify_build_complete() + %{state | analysis_ready?: true, awaiting_contracts: dirty} else state diff --git a/apps/language_server/lib/language_server/source_file.ex b/apps/language_server/lib/language_server/source_file.ex index bb2f7142c..d2e6e9844 100644 --- a/apps/language_server/lib/language_server/source_file.ex +++ b/apps/language_server/lib/language_server/source_file.ex @@ -113,6 +113,17 @@ defmodule ElixirLS.LanguageServer.SourceFile do IO.iodata_to_binary(Enum.reverse(acc)) end + def module_line(module) do + # TODO: Don't call into here directly + case ElixirSense.Core.Normalized.Code.get_docs(module, :moduledoc) do + nil -> + nil + + {line, _} -> + line + end + end + def function_line(mod, fun, arity) do # TODO: Don't call into here directly case ElixirSense.Core.Normalized.Code.get_docs(mod, :docs) do @@ -121,7 +132,7 @@ defmodule ElixirLS.LanguageServer.SourceFile do docs -> Enum.find_value(docs, fn - {{^fun, ^arity}, line, :def, _, _} -> line + {{^fun, ^arity}, line, _, _, _} -> line _ -> nil end) end diff --git a/apps/language_server/test/providers/workspace_symbols_test.ex b/apps/language_server/test/providers/workspace_symbols_test.ex new file mode 100644 index 000000000..ae0085bfc --- /dev/null +++ b/apps/language_server/test/providers/workspace_symbols_test.ex @@ -0,0 +1,228 @@ +defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do + alias ElixirLS.LanguageServer.Providers.WorkspaceSymbols + use ExUnit.Case + import ExUnit.CaptureLog + + setup_all do + pid = + case WorkspaceSymbols.start_link([]) do + {:ok, pid} -> pid + {:error, {:already_started, pid}} -> pid + end + + state = :sys.get_state(pid) + + 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 | modified_uris: [fixture_uri]} end) + + WorkspaceSymbols.notify_build_complete() + + wait_until_indexed(pid) + + on_exit(fn -> + :sys.replace_state(pid, fn _ -> state end) + end) + + {:ok, %{}} + end + + test "empty query" do + assert {:ok, []} == WorkspaceSymbols.symbols("") + end + + test "returns modules" do + assert {:ok, + [ + %{ + kind: 2, + location: %{ + range: %{end: %{character: 0, line: 1}, start: %{character: 0, line: 0}}, + uri: + "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + }, + name: "ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols" + } + ]} == WorkspaceSymbols.symbols("ElixirLS.LanguageServer.Fixtures.") + + assert {:ok, + [ + %{ + kind: 2, + location: %{ + range: %{end: %{character: 0, line: 1}, start: %{character: 0, line: 0}}, + uri: + "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + }, + name: "ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols" + } + ]} == WorkspaceSymbols.symbols("work") + end + + test "returns functions" do + assert { + :ok, + [ + %{ + kind: 12, + location: %{ + range: %{end: %{character: 0, line: 1}, start: %{character: 0, line: 0}}, + uri: + "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + }, + name: "f ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.module_info/1" + }, + %{ + kind: 12, + location: %{ + range: %{end: %{character: 0, line: 1}, start: %{character: 0, line: 0}}, + uri: + "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + }, + name: "f ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.module_info/0" + }, + %{ + kind: 12, + location: %{ + range: %{end: %{character: 0, line: 1}, start: %{character: 0, line: 0}}, + uri: + "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + }, + name: "f ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.behaviour_info/1" + }, + %{ + kind: 12, + location: %{ + range: %{end: %{character: 0, line: 3}, start: %{character: 0, line: 2}}, + uri: + "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + }, + name: "f ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.some_macro/1" + }, + %{ + kind: 12, + location: %{ + range: %{end: %{character: 0, line: 2}, start: %{character: 0, line: 1}}, + uri: + "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + }, + name: "f ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.some_function/1" + }, + %{ + kind: 12, + location: %{ + range: %{end: %{character: 0, line: 1}, start: %{character: 0, line: 0}}, + uri: + "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + }, + name: "f ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.__info__/1" + } + ] + } == WorkspaceSymbols.symbols("f ElixirLS.LanguageServer.Fixtures.") + + assert {:ok, + [ + %{ + kind: 12, + location: %{ + range: %{end: %{character: 0, line: 2}, start: %{character: 0, line: 1}}, + uri: + "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + }, + name: "f ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.some_function/1" + } + ]} == WorkspaceSymbols.symbols("f fun") + end + + test "returns types" do + assert { + :ok, + [ + %{ + kind: 5, + location: %{ + range: %{end: %{character: 0, line: 8}, start: %{character: 0, line: 7}}, + uri: + "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + }, + name: "t ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.some_type/0" + }, + %{ + kind: 5, + location: %{ + range: %{end: %{character: 0, line: 9}, start: %{character: 0, line: 8}}, + uri: + "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + }, + name: "t ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.some_opaque_type/0" + } + ] + } == WorkspaceSymbols.symbols("t ElixirLS.LanguageServer.Fixtures.") + + assert { + :ok, + [ + %{ + kind: 5, + location: %{ + range: %{end: %{character: 0, line: 9}, start: %{character: 0, line: 8}}, + uri: + "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + }, + name: "t ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.some_opaque_type/0" + } + ] + } == WorkspaceSymbols.symbols("t opa") + end + + test "returns callbacks" do + assert { + :ok, + [ + %{ + kind: 24, + location: %{ + range: %{end: %{character: 0, line: 5}, start: %{character: 0, line: 4}}, + uri: + "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + }, + name: "c ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.some_callback/1" + }, + %{ + kind: 24, + location: %{ + range: %{end: %{character: 0, line: 6}, start: %{character: 0, line: 5}}, + uri: + "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + }, + name: "c ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.some_macrocallback/1" + } + ] + } == WorkspaceSymbols.symbols("c ElixirLS.LanguageServer.Fixtures.") + + assert {:ok, + [ + %{ + kind: 24, + location: %{ + range: %{end: %{character: 0, line: 6}, start: %{character: 0, line: 5}}, + uri: + "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + }, + name: "c ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.some_macrocallback/1" + } + ]} == WorkspaceSymbols.symbols("c macr") + 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 + Process.sleep(500) + wait_until_indexed(pid) + end + end +end diff --git a/apps/language_server/test/support/fixtures/workspace_symbols.ex b/apps/language_server/test/support/fixtures/workspace_symbols.ex new file mode 100644 index 000000000..a9eba566f --- /dev/null +++ b/apps/language_server/test/support/fixtures/workspace_symbols.ex @@ -0,0 +1,10 @@ +defmodule ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols do + def some_function(a), do: a + defmacro some_macro(a), do: Macro.expand(a, __CALLER__) + + @callback some_callback(integer) :: atom + @callback some_macrocallback(integer) :: Macro.t() + + @type some_type :: atom + @type some_opaque_type :: atom +end From dee6a2e47db919169bac3a9c771df0abec9ae7e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Mon, 10 Feb 2020 12:06:37 +0100 Subject: [PATCH 02/13] Update apps/language_server/lib/language_server/providers/workspace_symbols.ex Co-Authored-By: Jason Axelson --- .../lib/language_server/providers/workspace_symbols.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a09fb7332..32a2965b7 100644 --- a/apps/language_server/lib/language_server/providers/workspace_symbols.ex +++ b/apps/language_server/lib/language_server/providers/workspace_symbols.ex @@ -1,6 +1,6 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do @moduledoc """ - Workspace Symbols provider. Generates and returns `SymbolInfo[]`. + Workspace Symbols provider. Generates and returns `SymbolInformation[]`. https://microsoft.github.io//language-server-protocol/specifications/specification-3-14/#workspace_symbol """ From ecf720ec1baf1aaea276ffd09ac1fc5e3d52446f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Mon, 10 Feb 2020 12:06:53 +0100 Subject: [PATCH 03/13] Update apps/language_server/lib/language_server/providers/workspace_symbols.ex Co-Authored-By: Jason Axelson --- .../lib/language_server/providers/workspace_symbols.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 32a2965b7..6617959f2 100644 --- a/apps/language_server/lib/language_server/providers/workspace_symbols.ex +++ b/apps/language_server/lib/language_server/providers/workspace_symbols.ex @@ -2,7 +2,7 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do @moduledoc """ Workspace Symbols provider. Generates and returns `SymbolInformation[]`. - https://microsoft.github.io//language-server-protocol/specifications/specification-3-14/#workspace_symbol + https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#workspace_symbol """ use GenServer From 77e0a97e05bf77291662b15ebda7d43698083167 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 11 Feb 2020 16:51:53 +0100 Subject: [PATCH 04/13] optimise short queries --- .../providers/workspace_symbols.ex | 38 ++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) 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 6617959f2..184f4f6b5 100644 --- a/apps/language_server/lib/language_server/providers/workspace_symbols.ex +++ b/apps/language_server/lib/language_server/providers/workspace_symbols.ex @@ -11,6 +11,8 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do alias ElixirLS.LanguageServer.Providers.SymbolUtils alias ElixirLS.LanguageServer.JsonRpc + @arity_suffix_regex ~r/\/\d+$/ + @type position_t :: %{ line: non_neg_integer, character: non_neg_integer @@ -235,12 +237,10 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do end end - defp get_score(item, query) do + defp get_score(item, query, query_downcase, query_length, arity_suffix) do item_downcase = String.downcase(item) - query_downcase = String.downcase(query) parts = item |> String.split(".") - arity_suffix = Regex.run(~r/\/\d+$/, query) cond do # searching for an erlang module but item is an Elixir module @@ -259,24 +259,40 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do arity_suffix != nil and not String.ends_with?(item, arity_suffix) -> 0.0 - length(parts) > 1 and Enum.at(parts, -1) |> String.contains?(query) -> + 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() |> String.contains?(query_downcase) -> + Enum.at(parts, -1) + |> String.downcase() + |> exact_or_contains?(query_downcase, query_length) -> 1.8 - String.contains?(item, query) -> + exact_or_contains?(item, query, query_length) -> 1.3 - String.contains?(item_downcase, query_downcase) -> + exact_or_contains?(item_downcase, query_downcase, query_length) -> 1.2 - true -> + query_length >= 3 -> String.jaro_distance(item_downcase, query_downcase) + + true -> + 0.0 end end + defp exact_or_contains?(string, needle = "/" <> _, needle_length) when needle_length < 3 do + String.ends_with?(string, needle) + end + + 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 + + defp exact_or_contains?(string, needle, _needle_length), do: String.contains?(string, needle) + defp limit_results(list) do list |> Enum.sort_by(&elem(&1, 1), &>=/2) @@ -386,11 +402,15 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do @spec get_results(state_t, key_t, String.t()) :: [symbol_information_t] defp get_results(state, key, query) do + query_downcase = String.downcase(query) + query_length = String.length(query) + arity_suffix = Regex.run(@arity_suffix_regex, query) + state |> Map.fetch!(key) |> chunk_by_schedulers(fn chunk -> chunk - |> Enum.map(&{&1, get_score(&1.name, query)}) + |> 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 From 24cda2bc48e66115eb7063cb26b31a600e8cbc57 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 11 Feb 2020 16:54:54 +0100 Subject: [PATCH 05/13] fix warning --- apps/language_server/test/providers/workspace_symbols_test.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/language_server/test/providers/workspace_symbols_test.ex b/apps/language_server/test/providers/workspace_symbols_test.ex index ae0085bfc..308ed5c4d 100644 --- a/apps/language_server/test/providers/workspace_symbols_test.ex +++ b/apps/language_server/test/providers/workspace_symbols_test.ex @@ -1,7 +1,6 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do alias ElixirLS.LanguageServer.Providers.WorkspaceSymbols use ExUnit.Case - import ExUnit.CaptureLog setup_all do pid = From b881f8fad728259ee303b9ebd7f697a5f775f4bf Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 11 Feb 2020 17:45:53 +0100 Subject: [PATCH 06/13] server state refactored --- .../providers/workspace_symbols.ex | 51 ++++++++++++++----- ...ols_test.ex => workspace_symbols_test.exs} | 7 ++- 2 files changed, 44 insertions(+), 14 deletions(-) rename apps/language_server/test/providers/{workspace_symbols_test.ex => workspace_symbols_test.exs} (97%) 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 184f4f6b5..2b5b324b6 100644 --- a/apps/language_server/lib/language_server/providers/workspace_symbols.ex +++ b/apps/language_server/lib/language_server/providers/workspace_symbols.ex @@ -92,9 +92,14 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do {:ok, %{ modules: [], + modules_indexed: false, types: [], + types_indexed: false, callbacks: [], + callbacks_indexed: false, functions: [], + functions_indexed: false, + indexing: false, modified_uris: [] }} end @@ -110,7 +115,14 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do end @impl GenServer - def handle_cast(:build_complete, %{modified_uris: []} = state) do + # 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 = @@ -126,11 +138,15 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do index(module_paths) - {:noreply, state} + {:noreply, %{state | indexing: true}} end @impl GenServer - def handle_cast(:build_complete, %{modified_uris: modified_uris} = state) do + # 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 = @@ -169,29 +185,38 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do %{ 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 = - if state.modules == [] or state.types == [] or state.callbacks == [] or - state.functions == [] do - state - else - %{state | modified_uris: uris ++ state.modified_uris} - end + state = %{state | modified_uris: uris ++ state.modified_uris} {:noreply, state} end @impl GenServer - def handle_info({:results, key, results}, state) do - {:noreply, state |> Map.put(key, results ++ state[key])} + 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 @@ -391,7 +416,7 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do Task.start_link(fn -> results = fun.() - send(self, {:results, key, results}) + send(self, {:indexing_complete, key, results}) JsonRpc.log_message( :info, diff --git a/apps/language_server/test/providers/workspace_symbols_test.ex b/apps/language_server/test/providers/workspace_symbols_test.exs similarity index 97% rename from apps/language_server/test/providers/workspace_symbols_test.ex rename to apps/language_server/test/providers/workspace_symbols_test.exs index 308ed5c4d..a836f7a07 100644 --- a/apps/language_server/test/providers/workspace_symbols_test.ex +++ b/apps/language_server/test/providers/workspace_symbols_test.exs @@ -16,7 +16,12 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do |> List.to_string() |> ElixirLS.LanguageServer.SourceFile.path_to_uri() - :sys.replace_state(pid, fn _ -> %{state | modified_uris: [fixture_uri]} end) + :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() From 1aa1786feb1b0acb37d2e7875a4696cb8d4fd895 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Tue, 11 Feb 2020 18:19:45 +0100 Subject: [PATCH 07/13] avoid reiterating the same enumerable --- .../providers/workspace_symbols.ex | 78 ++++++++++++------- .../test/providers/workspace_symbols_test.exs | 16 ++-- 2 files changed, 60 insertions(+), 34 deletions(-) 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 2b5b324b6..bf4c5e927 100644 --- a/apps/language_server/lib/language_server/providers/workspace_symbols.ex +++ b/apps/language_server/lib/language_server/providers/workspace_symbols.ex @@ -116,18 +116,21 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do @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 + 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() - |> chunk_by_schedulers(fn chunk -> + |> process_chunked(fn chunk -> for {module, beam_file} <- chunk, path = find_module_path(module, beam_file), path != nil, @@ -143,15 +146,18 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do @impl GenServer # indexed but some uris were modified - def handle_cast(:build_complete, %{ - indexing: false, - modified_uris: modified_uris = [_ | _] - } = state) do + 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() - |> chunk_by_schedulers(fn chunk -> + |> process_chunked(fn chunk -> for {module, beam_file} <- chunk, path = find_module_path(module, beam_file), SourceFile.path_to_uri(path) in modified_uris, @@ -196,6 +202,7 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do modified_uris: [] }} end + # indexed and no uris momified or already indexing def handle_cast(:build_complete, state) do {:noreply, state} @@ -210,13 +217,16 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do @impl GenServer def handle_info({:indexing_complete, key, results}, state) do - state = state - |> Map.put(key, results ++ state[key]) - |> Map.put(:"#{key}_indexed", true) + 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 + indexed = + state.modules_indexed and state.functions_indexed and state.types_indexed and + state.callbacks_indexed - {:noreply, %{state | indexing: not(indexed)}} + {:noreply, %{state | indexing: not indexed}} end ## Helpers @@ -352,9 +362,11 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do end defp index(module_paths) do + chunked_module_paths = chunk_by_schedulers(module_paths) + index_async(:modules, fn -> - module_paths - |> chunk_by_schedulers(fn chunk -> + chunked_module_paths + |> do_process_chunked(fn chunk -> for {module, path} <- chunk do line = find_module_line(module, path) build_result(:modules, module, path, line) @@ -363,8 +375,8 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do end) index_async(:functions, fn -> - module_paths - |> chunk_by_schedulers(fn chunk -> + chunked_module_paths + |> do_process_chunked(fn chunk -> for {module, path} <- chunk, {function, arity} <- module.module_info(:exports) do {function, arity} = strip_macro_prefix({function, arity}) @@ -376,8 +388,8 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do end) index_async(:types, fn -> - module_paths - |> chunk_by_schedulers(fn chunk -> + chunked_module_paths + |> do_process_chunked(fn chunk -> for {module, path} <- chunk, # TODO: Don't call into here directly {kind, {type, type_ast, args}} <- @@ -395,8 +407,8 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do end) index_async(:callbacks, fn -> - module_paths - |> chunk_by_schedulers(fn chunk -> + 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 @@ -433,7 +445,7 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do state |> Map.fetch!(key) - |> chunk_by_schedulers(fn chunk -> + |> 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) @@ -441,7 +453,7 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do |> limit_results end - defp chunk_by_schedulers(enumerable, fun) do + defp chunk_by_schedulers(enumerable) do chunk_size = Enum.count(enumerable) |> div(System.schedulers_online()) @@ -449,6 +461,16 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbols do enumerable |> Enum.chunk_every(chunk_size) + end + + defp process_chunked(enumerable, fun) do + enumerable + |> chunk_by_schedulers + |> do_process_chunked(fun) + end + + defp do_process_chunked(chunked_enumerable, fun) do + chunked_enumerable |> Enum.map(fn chunk when is_list(chunk) -> Task.async(fn -> fun.(chunk) diff --git a/apps/language_server/test/providers/workspace_symbols_test.exs b/apps/language_server/test/providers/workspace_symbols_test.exs index a836f7a07..ed67a2b52 100644 --- a/apps/language_server/test/providers/workspace_symbols_test.exs +++ b/apps/language_server/test/providers/workspace_symbols_test.exs @@ -16,12 +16,16 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do |> 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) + :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() From 72db8359a7383c097ae2881918520266021f3fec Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 13 Feb 2020 13:42:41 +0100 Subject: [PATCH 08/13] add readme note on workspace symbols queries --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index efe85b8a3..e0bdeec6f 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ This fork started when [Jake Becker's repository](https://github.com/JakeBecker/ - Code formatter - Find references to functions and modules (Thanks to @mattbaker) - Quick symbol lookup in file (Thanks to @mattbaker) +- Quick symbol lookup in workspace and stdlib (both Elixir and erlang) (@lukaszsamson) ![Screenshot](images/screenshot.png?raw=true) @@ -83,6 +84,28 @@ You can control which warnings are shown using the `elixirLS.dialyzerWarnOpts` s ElixirLS's Dialyzer integration uses internal, undocumented Dialyzer APIs, and so it won't be robust against changes to these APIs in future Erlang versions. +## Workspace Symbols + +With Dialyzer integration enabled ElixirLS will build an index of symbols (modules, functions, types and callbacks). The symbols are taken from current workspace, all dependances and stdlib (Elixir and erlang). This feature enables quick navigation to symbol definitions. However due to sheer number of different symbols and fuzzy search utilized by the provider, ElixirLS uses query prefixes to improove serch results relevance. + +Use the following rules when navigating to workspace symbols: +* no prefix - serch for modules + * `:erl` + * `Enu` +* `f ` prefix - search for functions + * `f inse` + * `f :ets.inse` + * `f Enum.cou` + * `f count/0` +* `t ` prefix - search for types + * `t t/0` + * `t :erlang.time_u` + * `t DateTime.` +* `c ` prefix - search for callbacks + * `c handle_info` + * `c GenServer.in` + * `c :gen_statem` + ## Troubleshooting Basic troubleshooting steps: From 2e71ddb831291bb63f42f24bc8bf2fcae9ea4e00 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 13 Feb 2020 13:42:49 +0100 Subject: [PATCH 09/13] add known issue --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e0bdeec6f..e7c55112d 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,7 @@ If your code doesn't compile in ElixirLS, it may be because ElixirLS compiles co ## Known Issues * `.exs` files don't return compilation errors +* `workspaceSymbolProvider` capability currently requires enabled dialyzer ## Building and running From 72c22c6d4590bae51ed40fa18e79f5e37d0b1dd4 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 13 Feb 2020 13:46:24 +0100 Subject: [PATCH 10/13] rename test --- apps/language_server/test/providers/workspace_symbols_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/language_server/test/providers/workspace_symbols_test.exs b/apps/language_server/test/providers/workspace_symbols_test.exs index ed67a2b52..9114aee95 100644 --- a/apps/language_server/test/providers/workspace_symbols_test.exs +++ b/apps/language_server/test/providers/workspace_symbols_test.exs @@ -1,4 +1,4 @@ -defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do +defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbolsTest do alias ElixirLS.LanguageServer.Providers.WorkspaceSymbols use ExUnit.Case From 0a64aeb3ad2c59aea093a8a13bae2ece8a31cca0 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 13 Feb 2020 13:58:39 +0100 Subject: [PATCH 11/13] don't assert on exact uris --- .../test/providers/workspace_symbols_test.exs | 84 +++++++++---------- 1 file changed, 39 insertions(+), 45 deletions(-) diff --git a/apps/language_server/test/providers/workspace_symbols_test.exs b/apps/language_server/test/providers/workspace_symbols_test.exs index 9114aee95..2f025e9c5 100644 --- a/apps/language_server/test/providers/workspace_symbols_test.exs +++ b/apps/language_server/test/providers/workspace_symbols_test.exs @@ -49,12 +49,13 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbolsTest do kind: 2, location: %{ range: %{end: %{character: 0, line: 1}, start: %{character: 0, line: 0}}, - uri: - "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + uri: uri }, name: "ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols" } - ]} == WorkspaceSymbols.symbols("ElixirLS.LanguageServer.Fixtures.") + ]} = WorkspaceSymbols.symbols("ElixirLS.LanguageServer.Fixtures.") + + assert uri |> String.ends_with?("test/support/fixtures/workspace_symbols.ex") assert {:ok, [ @@ -62,12 +63,13 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbolsTest do kind: 2, location: %{ range: %{end: %{character: 0, line: 1}, start: %{character: 0, line: 0}}, - uri: - "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + uri: uri }, name: "ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols" } - ]} == WorkspaceSymbols.symbols("work") + ]} = WorkspaceSymbols.symbols("work") + + assert uri |> String.ends_with?("test/support/fixtures/workspace_symbols.ex") end test "returns functions" do @@ -78,58 +80,49 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbolsTest do kind: 12, location: %{ range: %{end: %{character: 0, line: 1}, start: %{character: 0, line: 0}}, - uri: - "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + uri: uri }, name: "f ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.module_info/1" }, %{ kind: 12, location: %{ - range: %{end: %{character: 0, line: 1}, start: %{character: 0, line: 0}}, - uri: - "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + range: %{end: %{character: 0, line: 1}, start: %{character: 0, line: 0}} }, name: "f ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.module_info/0" }, %{ kind: 12, location: %{ - range: %{end: %{character: 0, line: 1}, start: %{character: 0, line: 0}}, - uri: - "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + range: %{end: %{character: 0, line: 1}, start: %{character: 0, line: 0}} }, name: "f ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.behaviour_info/1" }, %{ kind: 12, location: %{ - range: %{end: %{character: 0, line: 3}, start: %{character: 0, line: 2}}, - uri: - "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + range: %{end: %{character: 0, line: 3}, start: %{character: 0, line: 2}} }, name: "f ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.some_macro/1" }, %{ kind: 12, location: %{ - range: %{end: %{character: 0, line: 2}, start: %{character: 0, line: 1}}, - uri: - "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + range: %{end: %{character: 0, line: 2}, start: %{character: 0, line: 1}} }, name: "f ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.some_function/1" }, %{ kind: 12, location: %{ - range: %{end: %{character: 0, line: 1}, start: %{character: 0, line: 0}}, - uri: - "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + range: %{end: %{character: 0, line: 1}, start: %{character: 0, line: 0}} }, name: "f ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.__info__/1" } ] - } == WorkspaceSymbols.symbols("f ElixirLS.LanguageServer.Fixtures.") + } = WorkspaceSymbols.symbols("f ElixirLS.LanguageServer.Fixtures.") + + assert uri |> String.ends_with?("test/support/fixtures/workspace_symbols.ex") assert {:ok, [ @@ -137,12 +130,13 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbolsTest do kind: 12, location: %{ range: %{end: %{character: 0, line: 2}, start: %{character: 0, line: 1}}, - uri: - "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + uri: uri }, name: "f ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.some_function/1" } - ]} == WorkspaceSymbols.symbols("f fun") + ]} = WorkspaceSymbols.symbols("f fun") + + assert uri |> String.ends_with?("test/support/fixtures/workspace_symbols.ex") end test "returns types" do @@ -153,22 +147,21 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbolsTest do kind: 5, location: %{ range: %{end: %{character: 0, line: 8}, start: %{character: 0, line: 7}}, - uri: - "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + uri: uri }, name: "t ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.some_type/0" }, %{ kind: 5, location: %{ - range: %{end: %{character: 0, line: 9}, start: %{character: 0, line: 8}}, - uri: - "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + range: %{end: %{character: 0, line: 9}, start: %{character: 0, line: 8}} }, name: "t ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.some_opaque_type/0" } ] - } == WorkspaceSymbols.symbols("t ElixirLS.LanguageServer.Fixtures.") + } = WorkspaceSymbols.symbols("t ElixirLS.LanguageServer.Fixtures.") + + assert uri |> String.ends_with?("test/support/fixtures/workspace_symbols.ex") assert { :ok, @@ -177,13 +170,14 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbolsTest do kind: 5, location: %{ range: %{end: %{character: 0, line: 9}, start: %{character: 0, line: 8}}, - uri: - "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + uri: uri }, name: "t ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.some_opaque_type/0" } ] - } == WorkspaceSymbols.symbols("t opa") + } = WorkspaceSymbols.symbols("t opa") + + assert uri |> String.ends_with?("test/support/fixtures/workspace_symbols.ex") end test "returns callbacks" do @@ -194,22 +188,21 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbolsTest do kind: 24, location: %{ range: %{end: %{character: 0, line: 5}, start: %{character: 0, line: 4}}, - uri: - "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + uri: uri }, name: "c ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.some_callback/1" }, %{ kind: 24, location: %{ - range: %{end: %{character: 0, line: 6}, start: %{character: 0, line: 5}}, - uri: - "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + range: %{end: %{character: 0, line: 6}, start: %{character: 0, line: 5}} }, name: "c ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.some_macrocallback/1" } ] - } == WorkspaceSymbols.symbols("c ElixirLS.LanguageServer.Fixtures.") + } = WorkspaceSymbols.symbols("c ElixirLS.LanguageServer.Fixtures.") + + assert uri |> String.ends_with?("test/support/fixtures/workspace_symbols.ex") assert {:ok, [ @@ -217,12 +210,13 @@ defmodule ElixirLS.LanguageServer.Providers.WorkspaceSymbolsTest do kind: 24, location: %{ range: %{end: %{character: 0, line: 6}, start: %{character: 0, line: 5}}, - uri: - "file:///Users/lukaszsamson/vscode-elixir-ls/elixir-ls/apps/language_server/test/support/fixtures/workspace_symbols.ex" + uri: uri }, name: "c ElixirLS.LanguageServer.Fixtures.WorkspaceSymbols.some_macrocallback/1" } - ]} == WorkspaceSymbols.symbols("c macr") + ]} = WorkspaceSymbols.symbols("c macr") + + assert uri |> String.ends_with?("test/support/fixtures/workspace_symbols.ex") end defp wait_until_indexed(pid) do From 13d3555d24c5324e4051f00d9a50eeae198289cb Mon Sep 17 00:00:00 2001 From: Jason Axelson Date: Thu, 13 Feb 2020 07:04:41 -1000 Subject: [PATCH 12/13] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e7c55112d..92e590add 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ ElixirLS's Dialyzer integration uses internal, undocumented Dialyzer APIs, and s With Dialyzer integration enabled ElixirLS will build an index of symbols (modules, functions, types and callbacks). The symbols are taken from current workspace, all dependances and stdlib (Elixir and erlang). This feature enables quick navigation to symbol definitions. However due to sheer number of different symbols and fuzzy search utilized by the provider, ElixirLS uses query prefixes to improove serch results relevance. Use the following rules when navigating to workspace symbols: -* no prefix - serch for modules +* no prefix - search for modules * `:erl` * `Enu` * `f ` prefix - search for functions From 0cdd7689a7408250cc891011e631140f6dfa1a34 Mon Sep 17 00:00:00 2001 From: Jason Axelson Date: Thu, 13 Feb 2020 07:04:57 -1000 Subject: [PATCH 13/13] Update README.md spelling --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 92e590add..66bbb863c 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ ElixirLS's Dialyzer integration uses internal, undocumented Dialyzer APIs, and s ## Workspace Symbols -With Dialyzer integration enabled ElixirLS will build an index of symbols (modules, functions, types and callbacks). The symbols are taken from current workspace, all dependances and stdlib (Elixir and erlang). This feature enables quick navigation to symbol definitions. However due to sheer number of different symbols and fuzzy search utilized by the provider, ElixirLS uses query prefixes to improove serch results relevance. +With Dialyzer integration enabled ElixirLS will build an index of symbols (modules, functions, types and callbacks). The symbols are taken from the current workspace, all dependencies and stdlib (Elixir and erlang). This feature enables quick navigation to symbol definitions. However due to sheer number of different symbols and fuzzy search utilized by the provider, ElixirLS uses query prefixes to improve search results relevance. Use the following rules when navigating to workspace symbols: * no prefix - search for modules