diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 852d0279f..e474ddea4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,6 +67,16 @@ jobs: - elixir: 1.17.x otp: 27.x tests_may_fail: false + # https://github.com/erlef/setup-beam/issues/314 + # - elixir: 1.18.x + # otp: 25.x + # tests_may_fail: false + - elixir: 1.18.x + otp: 26.x + tests_may_fail: false + - elixir: 1.18.x + otp: 27.x + tests_may_fail: false env: MIX_ENV: test steps: @@ -80,7 +90,8 @@ jobs: mix local.hex --force mix local.rebar --force mix deps.get --only test - - run: mix test || ${{ matrix.tests_may_fail }} + - run: mix test + continue-on-error: ${{ matrix.tests_may_fail }} mix_test_windows: name: mix test windows (Elixir ${{matrix.elixir}} | Erlang/OTP ${{matrix.otp}}) @@ -126,6 +137,16 @@ jobs: - elixir: 1.17.x otp: 27.x tests_may_fail: false + # https://github.com/erlef/setup-beam/issues/314 + # - elixir: 1.18.x + # otp: 25.x + # tests_may_fail: false + - elixir: 1.18.x + otp: 26.x + tests_may_fail: false + - elixir: 1.18.x + otp: 27.x + tests_may_fail: false env: MIX_ENV: test steps: @@ -143,6 +164,7 @@ jobs: mix local.rebar --force mix deps.get --only test - run: mix test + continue-on-error: ${{ matrix.tests_may_fail }} static_analysis: name: static analysis (Elixir ${{matrix.elixir}} | Erlang/OTP ${{matrix.otp}}) @@ -150,7 +172,7 @@ jobs: strategy: matrix: include: - - elixir: 1.17.x + - elixir: 1.18.x otp: 27.x steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release-asset.yml b/.github/workflows/release-asset.yml index 43a49d516..d3fdd4103 100644 --- a/.github/workflows/release-asset.yml +++ b/.github/workflows/release-asset.yml @@ -42,7 +42,7 @@ jobs: - name: Set up BEAM uses: erlef/setup-beam@v1 with: - elixir-version: 1.17.x + elixir-version: 1.18.x otp-version: 27.x - name: Install dependencies diff --git a/README.md b/README.md index 403d53733..878a2ffd5 100644 --- a/README.md +++ b/README.md @@ -120,12 +120,12 @@ ElixirLS generally aims to support all supported versions of Elixir on all compa | 22 | 1.13 | Yes | Erlang docs not working (requires EIP 48) | | 23 | 1.13 - 1.14 | Yes | None | | 24 | 1.13 - 1.16 | Yes | None | -| 25 | 1.13.4 - 1.17 | Yes | None | +| 25 | 1.13.4 - 1.18 | Yes | None | | 26.0.0 - 26.0.1 | any | No | [#886](https://github.com/elixir-lsp/elixir-ls/issues/886) | -| 26.0.2 - 26.1.2 | 1.14.5 - 1.17 | *nix only | [#927](https://github.com/elixir-lsp/elixir-ls/issues/927), [#1023](https://github.com/elixir-lsp/elixir-ls/issues/1023) | -| >= 26.2.0 | 1.14.5 - 1.17 | Yes | None | +| 26.0.2 - 26.1.2 | 1.14.5 - 1.18 | *nix only | [#927](https://github.com/elixir-lsp/elixir-ls/issues/927), [#1023](https://github.com/elixir-lsp/elixir-ls/issues/1023) | +| >= 26.2.0 | 1.14.5 - 1.18 | Yes | None | | any | 1.15.5 | Yes | Broken formatter [#975](https://github.com/elixir-lsp/elixir-ls/issues/975) | -| 27 | 1.17 | Yes | None | +| 27 | 1.17 - 1.18 | Yes | None | ### Version management diff --git a/apps/debug_adapter/lib/debug_adapter/server.ex b/apps/debug_adapter/lib/debug_adapter/server.ex index e2103e315..4883c14e2 100644 --- a/apps/debug_adapter/lib/debug_adapter/server.ex +++ b/apps/debug_adapter/lib/debug_adapter/server.ex @@ -1187,7 +1187,7 @@ defmodule ElixirLS.DebugAdapter.Server do {state, frame_ids} = ensure_frame_ids(state, pid, stack_frames) stack_frames_json = - for {%Frame{} = stack_frame, frame_id} <- List.zip([stack_frames, frame_ids]) do + for {%Frame{} = stack_frame, frame_id} <- Enum.zip([stack_frames, frame_ids]) do %{ "id" => frame_id, "name" => Stacktrace.Frame.name(stack_frame), diff --git a/apps/debug_adapter/lib/debug_adapter/stacktrace.ex b/apps/debug_adapter/lib/debug_adapter/stacktrace.ex index ef73c9dde..febc68298 100644 --- a/apps/debug_adapter/lib/debug_adapter/stacktrace.ex +++ b/apps/debug_adapter/lib/debug_adapter/stacktrace.ex @@ -65,7 +65,7 @@ defmodule ElixirLS.DebugAdapter.Stacktrace do [] _ -> - frames = List.zip([backtrace_rest, stack_frames(meta_pid, level)]) + frames = Enum.zip([backtrace_rest, stack_frames(meta_pid, level)]) for {{level, {mod, function, args}}, {level, {mod, line}, bindings}} <- frames do %Frame{ diff --git a/apps/debug_adapter/test/binding_test.exs b/apps/debug_adapter/test/binding_test.exs index f739316a4..53a28994c 100644 --- a/apps/debug_adapter/test/binding_test.exs +++ b/apps/debug_adapter/test/binding_test.exs @@ -28,7 +28,11 @@ defmodule ElixirLS.DebugAdapter.BindingTest do test "multiple versions" do assert [{:asd, "b"}] == - Binding.to_elixir_variable_names([{:_asd@1, "a"}, {:_asd@12, "b"}, {:_asd@11, "c"}]) + Binding.to_elixir_variable_names([ + {:_asd@1, "a"}, + {:_asd@12, "b"}, + {:_asd@11, "c"} + ]) end test "filter _" do diff --git a/apps/elixir_ls_utils/lib/launch.ex b/apps/elixir_ls_utils/lib/launch.ex index 7e2cf9b21..091134757 100644 --- a/apps/elixir_ls_utils/lib/launch.ex +++ b/apps/elixir_ls_utils/lib/launch.ex @@ -28,7 +28,7 @@ defmodule ElixirLS.Utils.Launch do load_dot_config() - # as of 1.15 mix supports two environment variables MIX_QUIET and MIX_DEBUG + # as of 1.18 mix supports environment variables MIX_QUIET, MIX_DEBUG, MIX_PROFILE # that are not important for our use cases :ok diff --git a/apps/elixir_ls_utils/test/complete_test.exs b/apps/elixir_ls_utils/test/complete_test.exs index df91a7e5f..00505651d 100644 --- a/apps/elixir_ls_utils/test/complete_test.exs +++ b/apps/elixir_ls_utils/test/complete_test.exs @@ -1029,7 +1029,7 @@ defmodule ElixirLS.Utils.CompletionEngineTest do # local call on var - if Version.match?(System.version(), "< 1.16.0-dev") do + if Version.match?(System.version(), "< 1.16.0") do assert [] == expand(~c"asd.(") assert [] == expand(~c"@asd.(") else diff --git a/apps/elixir_ls_utils/test/support/mix_test.case.ex b/apps/elixir_ls_utils/test/support/mix_test.case.ex index 511394773..09a9c89c8 100644 --- a/apps/elixir_ls_utils/test/support/mix_test.case.ex +++ b/apps/elixir_ls_utils/test/support/mix_test.case.ex @@ -89,7 +89,13 @@ defmodule ElixirLS.Utils.MixTest.Case do defmacro in_fixture(dir, which, block) do module = inspect(__CALLER__.module) - function = Atom.to_string(elem(__CALLER__.function, 0)) + + function = + case __CALLER__.function do + {f, _a} -> Atom.to_string(f) + nil -> nil + end + tmp = Path.join(module, function) quote do diff --git a/apps/language_server/lib/language_server/ast_utils.ex b/apps/language_server/lib/language_server/ast_utils.ex index 5b9eb1c77..cbcb3fb94 100644 --- a/apps/language_server/lib/language_server/ast_utils.ex +++ b/apps/language_server/lib/language_server/ast_utils.ex @@ -1,9 +1,5 @@ # This code has originally been a part of https://github.com/elixir-lsp/elixir_sense -# Copyright (c) 2017 Marlus Saraiva -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - defmodule ElixirLS.LanguageServer.AstUtils do import ElixirLS.LanguageServer.Protocol alias ElixirLS.LanguageServer.SourceFile @@ -14,24 +10,40 @@ defmodule ElixirLS.LanguageServer.AstUtils do def node_range(node, options \\ []) def node_range(atom, _options) when is_atom(atom), do: nil - def node_range([{{:__block__, _, [_]} = first, _} | _] = list, _options) do - case List.last(list) do - {_, last} -> - case {node_range(first), node_range(last)} do - {range(start_line, start_character, _, _), range(_, _, end_line, end_character)} -> - range(start_line, start_character, end_line, end_character) + def node_range([{{:__block__, meta, [_]} = first, _} | _] = list, _options) do + if Keyword.get(meta, :format) == :keyword or Keyword.has_key?(meta, :assoc) or + Version.match?(System.version(), "< 1.18.0-dev") do + case List.last(list) do + {_, last} -> + case {node_range(first), node_range(last)} do + {range(start_line, start_character, _, _), range(_, _, end_line, end_character)} -> + range(start_line, start_character, end_line, end_character) - _ -> - nil - end + _ -> + nil + end - _ -> - nil + _ -> + nil + end end end def node_range(list, _options) when is_list(list), do: nil + def node_range({{:__block__, meta, [_]} = first, last}, _options) do + if Keyword.get(meta, :format) == :keyword or Keyword.has_key?(meta, :assoc) or + Version.match?(System.version(), "< 1.18.0-dev") do + case {node_range(first), node_range(last)} do + {range(start_line, start_character, _, _), range(_, _, end_line, end_character)} -> + range(start_line, start_character, end_line, end_character) + + _ -> + nil + end + end + end + def node_range({:__block__, meta, args} = _ast, _options) do line = Keyword.get(meta, :line) column = Keyword.get(meta, :column) @@ -62,6 +74,18 @@ defmodule ElixirLS.LanguageServer.AstUtils do # 2 element tuple {end_location[:line] - 1, end_location[:column] - 1 + 1} + match?(kind when kind in [:atom, :keyword], Keyword.get(meta, :format)) -> + [literal] = args + + modifier = + if literal in [true, false, nil] do + 1 + else + 0 + end + + get_literal_end(literal, {line, column + modifier}, nil) + match?([_], args) -> [literal] = args delimiter = meta[:delimiter] @@ -96,10 +120,12 @@ defmodule ElixirLS.LanguageServer.AstUtils do line = Keyword.get(meta, :line) - 1 column = Keyword.get(meta, :column) - 1 {end_line, end_column} = get_eoe_by_formatting(ast, {line, column}, options) - # on elixir 1.15+ formatter changes charlist '' to ~c"" sigil so we need to correct columns + + # on elixir 1.15-1.17 formatter changes charlist '' to ~c"" sigil so we need to correct columns # if charlist is single line correction = - if end_line == line and Version.match?(System.version(), ">= 1.15.0-dev") do + if end_line == line and Version.match?(System.version(), ">= 1.15.0-dev") and + Version.match?(System.version(), "< 1.18.0-dev") do 2 else 0 @@ -165,18 +191,24 @@ defmodule ElixirLS.LanguageServer.AstUtils do nil end - match?({:., _, [Kernel, :to_string]}, form) -> - {line, column} + match?({:., _meta, [Kernel, :to_string]}, form) -> + if Keyword.get(meta, :from_interpolation) || + Version.match?(System.version(), "< 1.16.0-dev") do + {line, column} + end - match?({:., _, [Access, :get]}, form) and match?([_ | _], args) -> - [arg | _] = args + match?({:., _meta, [Access, :get]}, form) and match?([_ | _], args) -> + if Keyword.get(meta, :from_brackets) || + Version.match?(System.version(), "< 1.16.0-dev") do + [arg | _] = args - case node_range(arg) do - range(line, column, _, _) -> - {line, column} + case node_range(arg) do + range(line, column, _, _) -> + {line, column} - nil -> - nil + nil -> + nil + end end match?({:., _, [_ | _]}, form) -> diff --git a/apps/language_server/lib/language_server/diagnostics.ex b/apps/language_server/lib/language_server/diagnostics.ex index 0975ef6e0..1a8b4def5 100644 --- a/apps/language_server/lib/language_server/diagnostics.ex +++ b/apps/language_server/lib/language_server/diagnostics.ex @@ -472,8 +472,14 @@ defmodule ElixirLS.LanguageServer.Diagnostics do for typing_trace <- typing_traces, trace <- typing_trace.traces do case typing_trace.type do :variable -> - line = trace.meta |> Keyword.get(:line, 1) - column = trace.meta |> Keyword.get(:column, 1) + {line, column} = + case trace do + %{meta: meta} -> + {Keyword.get(meta, :line, 1), Keyword.get(meta, :column, 1)} + + _ -> + {Keyword.get(trace, :line, 1), Keyword.get(trace, :column, 1)} + end message = "given type: #{trace.formatted_type}" diff --git a/apps/language_server/lib/language_server/dialyzer/manifest.ex b/apps/language_server/lib/language_server/dialyzer/manifest.ex index 3323e3143..cfc77b638 100644 --- a/apps/language_server/lib/language_server/dialyzer/manifest.ex +++ b/apps/language_server/lib/language_server/dialyzer/manifest.ex @@ -167,7 +167,10 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Manifest do modules_to_paths = for app <- @erlang_apps ++ @elixir_apps, path <- - Path.join([SourceFile.Path.escape_for_wildcard(Application.app_dir(app)), "**/*.beam"]) + Path.join([ + SourceFile.Path.escape_for_wildcard(Application.app_dir(app)), + "**/*.beam" + ]) |> Path.wildcard(), into: %{}, do: {pathname_to_module(path), path |> String.to_charlist()} diff --git a/apps/language_server/lib/language_server/markdown_utils.ex b/apps/language_server/lib/language_server/markdown_utils.ex index 9a8df0ae4..3b8e5e67d 100644 --- a/apps/language_server/lib/language_server/markdown_utils.ex +++ b/apps/language_server/lib/language_server/markdown_utils.ex @@ -142,6 +142,17 @@ defmodule ElixirLS.LanguageServer.MarkdownUtils do "**Delegates to** #{inspect(m)}.#{f}/#{a}" end + defp get_metadata_entry_md({:behaviours, []}), do: nil + + defp get_metadata_entry_md({:behaviours, list}) + when is_list(list) do + "**Implements** #{Enum.map_join(list, ", ", &inspect/1)}" + end + + defp get_metadata_entry_md({:source_annos, _}), do: nil + + defp get_metadata_entry_md({:source_path, _}), do: nil + defp get_metadata_entry_md({:spark_opts, _}), do: nil defp get_metadata_entry_md({key, value}) do @@ -404,7 +415,7 @@ defmodule ElixirLS.LanguageServer.MarkdownUtils do @kernel_special_forms_exports Kernel.SpecialForms.__info__(:macros) @kernel_exports Kernel.__info__(:macros) ++ Kernel.__info__(:functions) - defp get_module_fun_arity("..///3"), do: {Kernel, :"..//", 3} + defp get_module_fun_arity("..///3"), do: {Kernel, :..//, 3} defp get_module_fun_arity("../2"), do: {Kernel, :.., 2} defp get_module_fun_arity("../0"), do: {Kernel, :.., 0} defp get_module_fun_arity("./2"), do: {Kernel.SpecialForms, :., 2} diff --git a/apps/language_server/lib/language_server/mix_tasks/format.ex b/apps/language_server/lib/language_server/mix_tasks/format.ex index 197808cfe..fc512a8a2 100644 --- a/apps/language_server/lib/language_server/mix_tasks/format.ex +++ b/apps/language_server/lib/language_server/mix_tasks/format.ex @@ -1,6 +1,6 @@ # This file includes modified code extracted from the elixir project. Namely: # -# https://github.com/elixir-lang/elixir/blob/v1.15.6/lib/mix/lib/mix/tasks/format.ex +# https://github.com/elixir-lang/elixir/blob/v1.18.0/lib/mix/lib/mix/tasks/format.ex # # The original code is licensed as follows: # @@ -77,6 +77,9 @@ defmodule Mix.Tasks.ElixirLSFormat do ## Task-specific options + * `--force` - force formatting to happen on all files, instead of + relying on cache. + * `--check-formatted` - checks that the file is already formatted. This is useful in pre-commit hooks and CI scripts if you want to reject contributions with unformatted code. If the check fails, @@ -99,6 +102,14 @@ defmodule Mix.Tasks.ElixirLSFormat do as `.heex`. Without passing this flag, it is assumed that the code being passed via stdin is valid Elixir code. Defaults to "stdin.exs". + * `--migrate` - enables the `:migrate` option, which should be able to + automatically fix some deprecation warnings but changes the AST. + This should be safe in typical projects, but there is a non-zero risk + of breaking code for meta-programming heavy projects that relied on a + specific AST. We recommend running this task in its separate commit and + reviewing its output before committing. See the "Migration formatting" + section in `Code.format_string!/2` for more information. + ## When to format code We recommend developers to format code directly in their editors, either @@ -153,9 +164,8 @@ defmodule Mix.Tasks.ElixirLSFormat do inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}", "posts/*.{md,markdown}"] ] - Remember that, when running the formatter with plugins, you must make - sure that your dependencies and your application have been compiled, - so the relevant plugin code can be loaded. Otherwise a warning is logged. + Notice that, when running the formatter with plugins, your code will be + compiled first. In addition, the order by which you input your plugins is the format order. So, in the above `.formatter.exs`, the `MixMarkdownFormatter` will format @@ -205,10 +215,13 @@ defmodule Mix.Tasks.ElixirLSFormat do no_exit: :boolean, dot_formatter: :string, dry_run: :boolean, - stdin_filename: :string + stdin_filename: :string, + force: :boolean, + migrate: :boolean ] - @manifest "cached_dot_formatter" + @manifest_timestamp "format_timestamp" + @manifest_dot_formatter "cached_dot_formatter" @manifest_vsn 2 @newline "\n" @@ -241,9 +254,9 @@ defmodule Mix.Tasks.ElixirLSFormat do @callback format(String.t(), Keyword.t()) :: String.t() @impl true - def run(args) do + def run(all_args) do cwd = File.cwd!() - {opts, args} = OptionParser.parse!(args, strict: @switches) + {opts, args} = OptionParser.parse!(all_args, strict: @switches) {dot_formatter, formatter_opts} = eval_dot_formatter(cwd, opts) if opts[:check_equivalent] do @@ -254,47 +267,74 @@ defmodule Mix.Tasks.ElixirLSFormat do Mix.raise("--no-exit can only be used together with --check-formatted") end - deps_paths = Mix.Project.deps_paths() - manifest_path = Mix.Project.manifest_path() - config_mtime = Mix.Project.config_mtime() - mix_project = Mix.Project.get() + opts = + Keyword.merge(opts, + deps_paths: Mix.Project.deps_paths(), + manifest_path: Mix.Project.manifest_path(), + config_mtime: Mix.Project.config_mtime(), + mix_project: Mix.Project.get() + ) {formatter_opts_and_subs, _sources} = - eval_deps_and_subdirectories( - cwd, - mix_project, - deps_paths, - manifest_path, - config_mtime, - dot_formatter, - formatter_opts, - [dot_formatter] - ) + eval_deps_and_subdirectories(cwd, dot_formatter, formatter_opts, [dot_formatter], opts) - formatter_opts_and_subs = load_plugins(formatter_opts_and_subs) + formatter_opts_and_subs = load_plugins(formatter_opts_and_subs, opts) + files = expand_args(args, cwd, dot_formatter, formatter_opts_and_subs, opts) - args - |> expand_args(cwd, dot_formatter, formatter_opts_and_subs, opts) - |> Task.async_stream(&format_file(&1, opts), ordered: false, timeout: :infinity) - |> Enum.reduce({[], []}, &collect_status/2) - |> check!(opts) + maybe_cache_timestamps(all_args, files, fn files -> + files + |> Task.async_stream(&format_file(&1, opts), ordered: false, timeout: :infinity) + |> Enum.reduce({[], []}, &collect_status/2) + |> check!(opts) + end) + end + + defp maybe_cache_timestamps([], files, fun) do + if Mix.Project.get() do + # We fetch the time from before we read files so any future + # change to files are still picked up by the formatter + timestamp = System.os_time(:second) + dir = Mix.Project.manifest_path() + manifest_timestamp = Path.join(dir, @manifest_timestamp) + manifest_dot_formatter = Path.join(dir, @manifest_dot_formatter) + last_modified = Mix.Utils.last_modified(manifest_timestamp) + sources = [Mix.Project.config_mtime(), manifest_dot_formatter, ".formatter.exs"] + + files = + if Mix.Utils.stale?(sources, [last_modified]) do + files + else + Enum.filter(files, fn {file, _opts} -> + Mix.Utils.last_modified(file) > last_modified + end) + end + + try do + fun.(files) + after + File.mkdir_p!(dir) + File.touch!(manifest_timestamp, timestamp) + end + else + fun.(files) + end end - defp load_plugins({formatter_opts, subs}) do + defp maybe_cache_timestamps([_ | _], files, fun), do: fun.(files) + + defp load_plugins({formatter_opts, subs}, opts) do plugins = Keyword.get(formatter_opts, :plugins, []) if not is_list(plugins) do - Mix.raise("Expected :plugins to return a list of directories, got: #{inspect(plugins)}") + Mix.raise("Expected :plugins to return a list of modules, got: #{inspect(plugins)}") end - # we may not make changes to code paths and/or compile - # if plugins != [] do - # Mix.Task.run("loadpaths", []) - # end - - # if not Enum.all?(plugins, &Code.ensure_loaded?/1) do - # Mix.Task.run("compile", []) - # end + plugins = + if plugins != [] do + Keyword.get(opts, :plugin_loader, &plugin_loader/1).(plugins) + else + [] + end for plugin <- plugins do cond do @@ -327,7 +367,21 @@ defmodule Mix.Tasks.ElixirLSFormat do end) {Keyword.put(formatter_opts, :sigils, sigils), - Enum.map(subs, fn {path, opts} -> {path, load_plugins(opts)} end)} + Enum.map(subs, fn {path, formatter_opts_and_subs} -> + {path, load_plugins(formatter_opts_and_subs, opts)} + end)} + end + + defp plugin_loader(plugins) do + if plugins != [] do + Mix.Task.run("loadpaths", []) + end + + if not Enum.all?(plugins, &Code.ensure_loaded?/1) do + Mix.Task.run("compile", []) + end + + plugins end @doc """ @@ -335,75 +389,88 @@ defmodule Mix.Tasks.ElixirLSFormat do be used for the given file. The function must be called with the contents of the file - to be formatted. The options are returned for reflection - purposes. + to be formatted. Keep in mind that a function is always + returned, even if it doesn't match any of the inputs + specified in the `formatter.exs`. You can retrieve the + `:inputs` from the returned options, alongside the `:root` + option, to validate if the returned file matches the given + `:root` and `:inputs`. + + ## Options + + * `:deps_paths` (since v1.18.0) - the dependencies path to be used to resolve + `import_deps`. It defaults to `Mix.Project.deps_paths`. + + * `:dot_formatter` - use the given file as the `dot_formatter` + root. If this option is not specified, it uses the default one. + The default one is cached, so use this option only if necessary. + + * `:plugin_loader` (since v1.18.0) - a function that receives a list of plugins, + which may or may not yet be loaded, and ensures all of them are + loaded. It must return a list of plugins, which is recommended + to be the exact same list given as argument. You may choose to + skip plugins, but then it means the code will be partially + formatted (as in the plugins will be skipped). By default, + this function calls `mix loadpaths` and then, if not enough, + `mix compile`. + + * `:root` - use the given root as the current working directory. """ @doc since: "1.13.0" def formatter_for_file(file, opts \\ []) do cwd = Keyword.get_lazy(opts, :root, &File.cwd!/0) - deps_paths = Keyword.get_lazy(opts, :deps_paths, &Mix.Project.deps_paths/0) - manifest_path = Keyword.get_lazy(opts, :manifest_path, &Mix.Project.manifest_path/0) - config_mtime = Keyword.get_lazy(opts, :config_mtime, &Mix.Project.config_mtime/0) - mix_project = Keyword.get_lazy(opts, :mix_project, &Mix.Project.get/0) + + opts = + Keyword.merge(opts, + deps_paths: Keyword.get_lazy(opts, :deps_paths, &Mix.Project.deps_paths/0), + manifest_path: Keyword.get_lazy(opts, :manifest_path, &Mix.Project.manifest_path/0), + config_mtime: Keyword.get_lazy(opts, :config_mtime, &Mix.Project.config_mtime/0), + mix_project: Keyword.get_lazy(opts, :mix_project, &Mix.Project.get/0) + ) + {dot_formatter, formatter_opts} = eval_dot_formatter(cwd, opts) {formatter_opts_and_subs, _sources} = - eval_deps_and_subdirectories( - cwd, - mix_project, - deps_paths, - manifest_path, - config_mtime, - dot_formatter, - formatter_opts, - [dot_formatter] - ) + eval_deps_and_subdirectories(cwd, dot_formatter, formatter_opts, [dot_formatter], opts) - formatter_opts_and_subs = load_plugins(formatter_opts_and_subs) + formatter_opts_and_subs = load_plugins(formatter_opts_and_subs, opts) find_formatter_and_opts_for_file( SourceFile.Path.expand(file, cwd), - formatter_opts_and_subs, - cwd + cwd, + formatter_opts_and_subs ) end - @doc """ - Returns formatter options to be used for the given file. - """ - @doc deprecated: "Use formatter_for_file/2 instead" + @doc false + @deprecated "Use formatter_for_file/2 instead" def formatter_opts_for_file(file, opts \\ []) do {_, formatter_opts} = formatter_for_file(file, opts) formatter_opts end defp eval_dot_formatter(cwd, opts) do - cond do - dot_formatter = opts[:dot_formatter] -> - {dot_formatter, eval_file_with_keyword_list(dot_formatter)} + {dot_formatter, format_opts} = + cond do + dot_formatter = opts[:dot_formatter] -> + {dot_formatter, eval_file_with_keyword_list(dot_formatter)} - File.regular?(Path.join(cwd, ".formatter.exs")) -> - dot_formatter = Path.join(cwd, ".formatter.exs") - {".formatter.exs", eval_file_with_keyword_list(dot_formatter)} + File.regular?(Path.join(cwd, ".formatter.exs")) -> + dot_formatter = Path.join(cwd, ".formatter.exs") + {".formatter.exs", eval_file_with_keyword_list(dot_formatter)} - true -> - {".formatter.exs", []} - end + true -> + {".formatter.exs", []} + end + + # the --migrate flag overrides settings from the dot formatter + {dot_formatter, Keyword.take(opts, [:migrate]) ++ format_opts} end # This function reads exported configuration from the imported # dependencies and subdirectories and deals with caching the result # of reading such configuration in a manifest file. - defp eval_deps_and_subdirectories( - cwd, - mix_project, - deps_paths, - manifest_path, - config_mtime, - dot_formatter, - formatter_opts, - sources - ) do + defp eval_deps_and_subdirectories(cwd, dot_formatter, formatter_opts, sources, opts) do deps = Keyword.get(formatter_opts, :import_deps, []) subs = Keyword.get(formatter_opts, :subdirectories, []) @@ -418,23 +485,19 @@ defmodule Mix.Tasks.ElixirLSFormat do if deps == [] and subs == [] do {{formatter_opts, []}, sources} else - manifest = Path.join(manifest_path, @manifest) + manifest = Path.join(opts[:manifest_path], @manifest_dot_formatter) {{locals_without_parens, subdirectories}, sources} = - maybe_cache_in_manifest(dot_formatter, mix_project, manifest, config_mtime, fn -> - {subdirectories, sources} = - eval_subs_opts( - subs, - cwd, - mix_project, - deps_paths, - manifest_path, - config_mtime, - sources - ) - - {{eval_deps_opts(deps, deps_paths), subdirectories}, sources} - end) + maybe_cache_in_manifest( + dot_formatter, + opts[:mix_project], + manifest, + opts[:config_mtime], + fn -> + {subdirectories, sources} = eval_subs_opts(subs, cwd, sources, opts) + {{eval_deps_opts(deps, opts), subdirectories}, sources} + end + ) formatter_opts = Keyword.update( @@ -479,11 +542,13 @@ defmodule Mix.Tasks.ElixirLSFormat do {entry, sources} end - defp eval_deps_opts([], _deps_paths) do + defp eval_deps_opts([], _opts) do [] end - defp eval_deps_opts(deps, deps_paths) do + defp eval_deps_opts(deps, opts) do + deps_paths = opts[:deps_paths] || Mix.Project.deps_paths() + for dep <- deps, dep_path = assert_valid_dep_and_fetch_path(dep, deps_paths), dep_dot_formatter = Path.join(dep_path, ".formatter.exs"), @@ -494,7 +559,7 @@ defmodule Mix.Tasks.ElixirLSFormat do do: parenless_call end - defp eval_subs_opts(subs, cwd, mix_project, deps_paths, manifest_path, config_mtime, sources) do + defp eval_subs_opts(subs, cwd, sources, opts) do {subs, sources} = Enum.flat_map_reduce(subs, sources, fn sub, sources -> cwd = SourceFile.Path.expand(sub, cwd) @@ -508,16 +573,7 @@ defmodule Mix.Tasks.ElixirLSFormat do formatter_opts = eval_file_with_keyword_list(sub_formatter) {formatter_opts_and_subs, sources} = - eval_deps_and_subdirectories( - sub, - mix_project, - deps_paths, - manifest_path, - config_mtime, - :in_memory, - formatter_opts, - sources - ) + eval_deps_and_subdirectories(sub, :in_memory, formatter_opts, sources, opts) {[{sub, formatter_opts_and_subs}], sources} else @@ -547,7 +603,7 @@ defmodule Mix.Tasks.ElixirLSFormat do defp eval_file_with_keyword_list(path) do {opts, _} = Code.eval_file(path) - unless Keyword.keyword?(opts) do + if not Keyword.keyword?(opts) do Mix.raise("Expected #{inspect(path)} to return a keyword list, got: #{inspect(opts)}") end @@ -588,14 +644,12 @@ defmodule Mix.Tasks.ElixirLSFormat do stdin_filename = SourceFile.Path.expand(Keyword.get(opts, :stdin_filename, "stdin.exs"), cwd) - {formatter, _opts, _dir} = - find_formatter_and_opts_for_file(stdin_filename, {formatter_opts, subs}, cwd) + {formatter, _opts} = + find_formatter_and_opts_for_file(stdin_filename, cwd, {formatter_opts, subs}) {file, formatter} else - {formatter, _opts, _dir} = - find_formatter_and_opts_for_file(file, {formatter_opts, subs}, cwd) - + {formatter, _opts} = find_formatter_and_opts_for_file(file, cwd, {formatter_opts, subs}) {file, formatter} end end @@ -661,22 +715,19 @@ defmodule Mix.Tasks.ElixirLSFormat do if plugins != [], do: plugins, else: nil end - defp find_formatter_and_opts_for_file(file, formatter_opts_and_subs, cwd) do - {formatter_opts, dir} = recur_formatter_opts_for_file(file, formatter_opts_and_subs) - {find_formatter_for_file(file, formatter_opts), formatter_opts, dir || cwd} + defp find_formatter_and_opts_for_file(file, root, formatter_opts_and_subs) do + {formatter_opts, root} = recur_formatter_opts_for_file(file, root, formatter_opts_and_subs) + {find_formatter_for_file(file, formatter_opts), [root: root] ++ formatter_opts} end - defp recur_formatter_opts_for_file(file, {formatter_opts, subs}) do - Enum.find_value(subs, {formatter_opts, nil}, fn {sub, formatter_opts_and_subs} -> + defp recur_formatter_opts_for_file(file, root, {formatter_opts, subs}) do + Enum.find_value(subs, {formatter_opts, root}, fn {sub, formatter_opts_and_subs} -> size = byte_size(sub) case file do <> when prefix == sub and dir_separator in [?\\, ?/] -> - case recur_formatter_opts_for_file(file, formatter_opts_and_subs) do - {nested_formatter_opts, nil} -> {nested_formatter_opts, sub} - {nested_formatter_opts, nested_sub} -> {nested_formatter_opts, nested_sub} - end + recur_formatter_opts_for_file(file, sub, formatter_opts_and_subs) _ -> nil @@ -987,7 +1038,7 @@ defmodule Mix.Tasks.ElixirLSFormat do if space? do str - |> String.split(~r/[\t\s]+/u, include_captures: true) + |> String.split(~r/[\t\s]+/, include_captures: true) |> Enum.map(fn <> = str when start in ["\t", "\s"] -> IO.ANSI.format([color[:space], str]) diff --git a/apps/language_server/lib/language_server/providers/code_lens/type_spec/contract_translator.ex b/apps/language_server/lib/language_server/providers/code_lens/type_spec/contract_translator.ex index fb21d82d1..e6b455954 100644 --- a/apps/language_server/lib/language_server/providers/code_lens/type_spec/contract_translator.ex +++ b/apps/language_server/lib/language_server/providers/code_lens/type_spec/contract_translator.ex @@ -144,13 +144,17 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.TypeSpec.ContractTranslator cond do Code.ensure_loaded?(mod) and function_exported?(mod, :__protocol__, 1) -> # defprotocol - case ast do - {:"::", [], [{:foo, [], [_ | rest_args]}, res]} -> + case {ast, fun} do + {ast, :__deriving__} -> + # do not change __deriving__ macrocallback + ast + + {{:"::", [], [{:foo, [], [_ | rest_args]}, res]}, _} -> # ordinary defs in defprotocol do not have when and have at least 1 arg # first arg in defprotocol defs is always of type t {:"::", [], [{:foo, [], [{:t, [], []} | rest_args]}, res]} - {:"::", [], [{:foo, [], []}, _]} -> + {{:"::", [], [{:foo, [], []}, _]}, _} -> # def with default arg ast end diff --git a/apps/language_server/lib/language_server/providers/completion/reducers/callbacks.ex b/apps/language_server/lib/language_server/providers/completion/reducers/callbacks.ex index d3535f1ea..73cd038ef 100644 --- a/apps/language_server/lib/language_server/providers/completion/reducers/callbacks.ex +++ b/apps/language_server/lib/language_server/providers/completion/reducers/callbacks.ex @@ -82,6 +82,8 @@ defmodule ElixirLS.LanguageServer.Providers.Completion.Reducers.Callbacks do metadata: metadata } <- Introspection.get_callbacks_with_docs(mod), + mod != Protocol or + {name, arity} not in [impl_for!: 1, impl_for: 1, __protocol__: 1], hint == "" or def_prefix?(hint, spec) or Matcher.match?("#{name}", hint) do desc = Introspection.extract_summary_from_docs(doc) diff --git a/apps/language_server/lib/language_server/providers/completion/reducers/protocol.ex b/apps/language_server/lib/language_server/providers/completion/reducers/protocol.ex index 04c024409..1ae27bcc5 100644 --- a/apps/language_server/lib/language_server/providers/completion/reducers/protocol.ex +++ b/apps/language_server/lib/language_server/providers/completion/reducers/protocol.ex @@ -69,6 +69,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion.Reducers.Protocol do else for {{name, arity}, {_type, args, docs, metadata, spec}} <- Introspection.module_functions_info(protocol), + not match?(%{implementing: Protocol}, metadata), hint == "" or String.starts_with?("def", hint) or Matcher.match?("#{name}", hint) do %{ type: :protocol_function, diff --git a/apps/language_server/lib/language_server/providers/completion/reducers/record.ex b/apps/language_server/lib/language_server/providers/completion/reducers/record.ex index 4f498ba3e..7ed3d69a0 100644 --- a/apps/language_server/lib/language_server/providers/completion/reducers/record.ex +++ b/apps/language_server/lib/language_server/providers/completion/reducers/record.ex @@ -7,6 +7,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion.Reducers.Record do alias ElixirSense.Core.State alias ElixirSense.Core.State.{RecordInfo, TypeInfo} alias ElixirLS.Utils.Matcher + alias ElixirSense.Core.Normalized.Code, as: NormalizedCode @type field :: %{ type: :field, @@ -95,8 +96,8 @@ defmodule ElixirLS.LanguageServer.Providers.Completion.Reducers.Record do cursor_position, not elixir_prefix ), - %RecordInfo{} = info <- records[{mod, fun}] do - fields = get_fields(hint, mod, fun, info.fields, options_so_far, metadata_types) + fields_info when is_list(fields_info) <- get_record_info(mod, fun, records) do + fields = get_fields(hint, mod, fun, fields_info, options_so_far, metadata_types) {fields, if(npar == 0 and cursor_at_option in [false, :maybe], do: :maybe_record_update)} else @@ -105,6 +106,27 @@ defmodule ElixirLS.LanguageServer.Providers.Completion.Reducers.Record do end end + defp get_record_info(mod, fun, records) do + case records[{mod, fun}] do + %RecordInfo{} = info -> + info.fields + + nil -> + if Version.match?(System.version(), ">= 1.18.0-dev") do + case NormalizedCode.get_docs(mod, :docs) do + nil -> + nil + + docs -> + Enum.find_value(docs, fn + {{^fun, 1}, _, :macro, _, _, %{record: {_tag, fields}}} -> fields + _ -> nil + end) + end + end + end + end + defp get_fields(hint, module, record_name, fields, fields_so_far, types) do field_types = get_field_types(types, module, record_name) @@ -154,7 +176,47 @@ defmodule ElixirLS.LanguageServer.Providers.Completion.Reducers.Record do when kind in [:type, :typep, :opaque] <- ast do field_types else - _ -> [] + _ -> + candidates = + if Version.match?(System.version(), ">= 1.18.0-dev") do + ElixirSense.Core.TypeInfo.find_all(module, fn info -> + info.name in [record, :"#{record}_t", :t] and info.arity == 0 + end) + else + [] + end + + with [info | _] <- candidates, + {:ok, ast} <- Code.string_to_quoted(info.spec), + {:@, _, + [ + {kind, _, + [ + {:"::", _, + [ + {_name, _, []}, + {:{}, _, + [ + _tag + | field_types + ]} + ]} + ]} + ]} + when kind in [:type, :typep, :opaque] <- ast do + field_types + |> Enum.map(fn + {:"::", _, [{name, _, context}, type]} when is_atom(name) and is_atom(context) -> + {name, type} + + _ -> + nil + end) + |> Enum.reject(&is_nil/1) + else + _ -> + [] + end end end end diff --git a/apps/language_server/lib/language_server/providers/execute_command/apply_spec.ex b/apps/language_server/lib/language_server/providers/execute_command/apply_spec.ex index dff6ad2ad..d4fada67b 100644 --- a/apps/language_server/lib/language_server/providers/execute_command/apply_spec.ex +++ b/apps/language_server/lib/language_server/providers/execute_command/apply_spec.ex @@ -57,7 +57,7 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.ApplySpec do try do target_line_length = case SourceFile.formatter_for(uri, state.project_dir, true) do - {:ok, {_, opts, _formatter_exs_dir}} -> + {:ok, {_, opts}} -> Keyword.get(opts, :line_length, @default_target_line_length) {:error, reason} -> diff --git a/apps/language_server/lib/language_server/providers/formatting.ex b/apps/language_server/lib/language_server/providers/formatting.ex index 8290a9ae7..7f9ade78e 100644 --- a/apps/language_server/lib/language_server/providers/formatting.ex +++ b/apps/language_server/lib/language_server/providers/formatting.ex @@ -12,7 +12,9 @@ defmodule ElixirLS.LanguageServer.Providers.Formatting do if SourceFile.Path.path_in_dir?(file_path, project_dir) do # file in project_dir we find formatter and options for file case SourceFile.formatter_for(uri, project_dir, mix_project?) do - {:ok, {formatter, opts, formatter_exs_dir}} -> + {:ok, {formatter, opts}} -> + formatter_exs_dir = opts[:root] + if should_format?(uri, formatter_exs_dir, opts[:inputs], project_dir) do do_format(source_file, formatter, opts) else diff --git a/apps/language_server/lib/language_server/providers/plugins/phoenix.ex b/apps/language_server/lib/language_server/providers/plugins/phoenix.ex index eb44bcb78..9f1569280 100644 --- a/apps/language_server/lib/language_server/providers/plugins/phoenix.ex +++ b/apps/language_server/lib/language_server/providers/plugins/phoenix.ex @@ -24,7 +24,7 @@ defmodule ElixirLS.LanguageServer.Plugins.Phoenix do ModuleStore.ensure_compiled(context, Phoenix.Router) end - if Version.match?(System.version(), ">= 1.14.0") do + if Version.match?(System.version(), ">= 1.14.0-dev") do @impl true def suggestions(hint, {Phoenix.Router, func, 1, _info}, _list, opts) when func in @phoenix_route_funcs do diff --git a/apps/language_server/lib/language_server/providers/selection_ranges.ex b/apps/language_server/lib/language_server/providers/selection_ranges.ex index d8298ccd5..0638082a9 100644 --- a/apps/language_server/lib/language_server/providers/selection_ranges.ex +++ b/apps/language_server/lib/language_server/providers/selection_ranges.ex @@ -397,6 +397,53 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRanges do acc end + ranges_acc = + case ast do + {_, meta, _} -> + parens_ranges = + for {:parens, parens_meta} <- meta, + parens_start_line = Keyword.fetch!(parens_meta, :line) - 1, + parens_start_character = Keyword.fetch!(parens_meta, :column) - 1, + parens_meta_closing = Keyword.fetch!(parens_meta, :closing), + parens_end_line = Keyword.fetch!(parens_meta_closing, :line) - 1, + parens_end_character = Keyword.fetch!(parens_meta_closing, :column), + (parens_start_line < line or + (parens_start_line == line and parens_start_character <= character)) and + (parens_end_line > line or + (parens_end_line == line and parens_end_character >= character)) do + # NOTE there may be multiple parens keys + outer_range = + range( + parens_start_line, + parens_start_character, + parens_end_line, + parens_end_character + ) + + if (parens_start_line < line or + (parens_start_line == line and parens_start_character + 1 <= character)) and + (parens_end_line > line or + (parens_end_line == line and parens_end_character - 1 >= character)) do + inner_range = + range( + parens_start_line, + parens_start_character + 1, + parens_end_line, + parens_end_character - 1 + ) + + [outer_range, inner_range] + else + [outer_range] + end + end + + List.flatten(parens_ranges) ++ ranges_acc + + _ -> + ranges_acc + end + parent_acc = if match?({_, _, _}, ast) do [ast | parent_ast] @@ -417,6 +464,8 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRanges do acc |> sort_ranges_widest_to_narrowest() + |> deduplicate + |> fix_properties end def ast_node_ranges(_, _, _, _), do: [] diff --git a/apps/language_server/lib/language_server/range_utils.ex b/apps/language_server/lib/language_server/range_utils.ex index dae9b0c45..c7612d028 100644 --- a/apps/language_server/lib/language_server/range_utils.ex +++ b/apps/language_server/lib/language_server/range_utils.ex @@ -180,4 +180,34 @@ defmodule ElixirLS.LanguageServer.RangeUtils do defp do_deduplicate([range | rest], acc) do do_deduplicate(rest, [range | acc]) end + + def fix_properties(ranges) do + ranges + |> Enum.reverse() + |> Enum.reduce([], fn + r, [] -> + [r] + + range(start_line, start_character, end_line, end_character), + [range(last_start_line, last_start_character, last_end_line, last_end_character) | _] = acc -> + new_start_line = min(start_line, last_start_line) + new_end_line = max(end_line, last_end_line) + + new_start_character = + if start_line < last_start_line do + start_character + else + min(start_character, last_start_character) + end + + new_end_character = + if end_line > last_end_line do + end_character + else + max(end_character, last_end_character) + end + + [range(new_start_line, new_start_character, new_end_line, new_end_character) | acc] + end) + end end diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index 3dd567c47..13b66a3c3 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -1096,7 +1096,7 @@ defmodule ElixirLS.LanguageServer.Server do locals_without_parens = case SourceFile.formatter_for(uri, state.project_dir, state.mix_project?) do - {:ok, {_, opts, _formatter_exs_dir}} -> + {:ok, {_, opts}} -> locals_without_parens = Keyword.get(opts, :locals_without_parens, []) if List.improper?(locals_without_parens) do @@ -1226,7 +1226,7 @@ defmodule ElixirLS.LanguageServer.Server do if String.ends_with?(uri, [".ex", ".exs"]) or source_file.language_id in ["elixir"] do formatter_opts = case SourceFile.formatter_for(uri, state.project_dir, state.mix_project?) do - {:ok, {_, opts, _formatter_exs_dir}} -> opts + {:ok, {_, opts}} -> opts {:error, _} -> [] end diff --git a/apps/language_server/lib/language_server/source_file.ex b/apps/language_server/lib/language_server/source_file.ex index 46e088908..a1af18dd0 100644 --- a/apps/language_server/lib/language_server/source_file.ex +++ b/apps/language_server/lib/language_server/source_file.ex @@ -257,7 +257,7 @@ defmodule ElixirLS.LanguageServer.SourceFile do end @spec formatter_for(String.t(), String.t() | nil, boolean) :: - {:ok, {function | nil, keyword(), String.t()}} | {:error, any} + {:ok, {function | nil, keyword()}} | {:error, any} def formatter_for(uri = "file:" <> _, project_dir, mix_project?) when is_binary(project_dir) do path = __MODULE__.Path.from_uri(uri) @@ -271,7 +271,12 @@ defmodule ElixirLS.LanguageServer.SourceFile do manifest_path: MixProjectCache.manifest_path(), config_mtime: MixProjectCache.config_mtime(), mix_project: MixProjectCache.get(), - root: project_dir + root: project_dir, + plugin_loader: fn _plugins -> + # we don't do any plugin loading as this may trigger compile + # TODO it may be safe to compile on 1.18+ + :ok + end ] {:ok, Mix.Tasks.ElixirLSFormat.formatter_for_file(path, opts)} diff --git a/apps/language_server/lib/language_server/tracer.ex b/apps/language_server/lib/language_server/tracer.ex index 89a2dabbc..fc0a56db6 100644 --- a/apps/language_server/lib/language_server/tracer.ex +++ b/apps/language_server/lib/language_server/tracer.ex @@ -3,11 +3,8 @@ defmodule ElixirLS.LanguageServer.Tracer do """ use GenServer alias ElixirLS.LanguageServer.JsonRpc - alias ElixirLS.LanguageServer.SourceFile require Logger - @version 3 - @tables ~w(modules calls)a for table <- @tables do @@ -84,7 +81,7 @@ defmodule ElixirLS.LanguageServer.Tracer do end @impl true - def terminate(reason, state) do + def terminate(reason, _state) do case reason do :normal -> :ok diff --git a/apps/language_server/test/ast_utils_test.exs b/apps/language_server/test/ast_utils_test.exs index c317ca594..9137a4a87 100644 --- a/apps/language_server/test/ast_utils_test.exs +++ b/apps/language_server/test/ast_utils_test.exs @@ -32,8 +32,17 @@ defmodule ElixirLS.LanguageServer.AstUtilsTest do assert get_range("nil") == range(0, 0, 0, 3) end + if Version.match?(System.version(), ">= 1.18.0") do + test "true as atom" do + assert get_range(":true") == range(0, 0, 0, 5) + end + end + test "integer" do assert get_range("1234") == range(0, 0, 0, 4) + + assert node_range({:__block__, [token: "2", line: 1, column: 10], [2]}) == + range(0, 9, 0, 10) end test "float" do @@ -246,8 +255,9 @@ defmodule ElixirLS.LanguageServer.AstUtilsTest do end # Parser is simplifying the expression and not including the parens + # we handle parens meta in selection ranges # test "nested binary operators with parens" do - # assert get_range("var * 3 * (foo + x)") == range(0, 0, 0, 17) + # assert get_range("var * 3 * (foo + x)") == range(0, 0, 0, 19) # end test "nested binary and unary operators" do diff --git a/apps/language_server/test/diagnostics_test.exs b/apps/language_server/test/diagnostics_test.exs index 864559435..4cd80bd75 100644 --- a/apps/language_server/test/diagnostics_test.exs +++ b/apps/language_server/test/diagnostics_test.exs @@ -2,7 +2,7 @@ defmodule ElixirLS.LanguageServer.DiagnosticsTest do alias ElixirLS.LanguageServer.Diagnostics use ExUnit.Case - if Version.match?(System.version(), "< 1.16.0-dev") do + if Version.match?(System.version(), "< 1.16.0") do describe "pre 1.16 Mix.Task.Compiler.Diagnostic normalization" do test "extract the stacktrace from the message and format it" do root_path = Path.join(__DIR__, "fixtures/build_errors") diff --git a/apps/language_server/test/dialyzer_incremental_test.exs b/apps/language_server/test/dialyzer_incremental_test.exs index 65b1a78c1..33f331ef4 100644 --- a/apps/language_server/test/dialyzer_incremental_test.exs +++ b/apps/language_server/test/dialyzer_incremental_test.exs @@ -64,8 +64,8 @@ if System.otp_release() |> String.to_integer() >= 26 do %{ "message" => error_message1, "range" => %{ - "end" => %{"character" => 2, "line" => 1}, - "start" => %{"character" => 2, "line" => 1} + "end" => %{"line" => 1}, + "start" => %{"line" => 1} }, "severity" => 2, "source" => "ElixirLS Dialyzer" @@ -73,8 +73,8 @@ if System.otp_release() |> String.to_integer() >= 26 do %{ "message" => error_message2, "range" => %{ - "end" => %{"character" => 4, "line" => 2}, - "start" => %{"character" => 4, "line" => 2} + "end" => %{"line" => 2}, + "start" => %{"line" => 2} }, "severity" => 2, "source" => "ElixirLS Dialyzer" @@ -126,8 +126,8 @@ if System.otp_release() |> String.to_integer() >= 26 do %{ "message" => error_message1, "range" => %{ - "end" => %{"character" => 2, "line" => 1}, - "start" => %{"character" => 2, "line" => 1} + "end" => %{"line" => 1}, + "start" => %{"line" => 1} }, "severity" => 2, "source" => "ElixirLS Dialyzer" @@ -135,8 +135,8 @@ if System.otp_release() |> String.to_integer() >= 26 do %{ "message" => error_message2, "range" => %{ - "end" => %{"character" => 4, "line" => 2}, - "start" => %{"character" => 4, "line" => 2} + "end" => %{"line" => 2}, + "start" => %{"line" => 2} }, "severity" => 2, "source" => "ElixirLS Dialyzer" @@ -172,8 +172,8 @@ if System.otp_release() |> String.to_integer() >= 26 do %{ "message" => error_message1, "range" => %{ - "end" => %{"character" => 2, "line" => 1}, - "start" => %{"character" => 2, "line" => 1} + "end" => %{"line" => 1}, + "start" => %{"line" => 1} }, "severity" => 2, "source" => "ElixirLS Dialyzer" @@ -181,8 +181,8 @@ if System.otp_release() |> String.to_integer() >= 26 do %{ "message" => error_message2, "range" => %{ - "end" => %{"character" => 4, "line" => 2}, - "start" => %{"character" => 4, "line" => 2} + "end" => %{"line" => 2}, + "start" => %{"line" => 2} }, "severity" => 2, "source" => "ElixirLS Dialyzer" @@ -208,8 +208,8 @@ if System.otp_release() |> String.to_integer() >= 26 do %{ "message" => error_message1, "range" => %{ - "end" => %{"character" => 2, "line" => 1}, - "start" => %{"character" => 2, "line" => 1} + "end" => %{"line" => 1}, + "start" => %{"line" => 1} }, "severity" => 2, "source" => "ElixirLS Dialyzer" @@ -217,8 +217,8 @@ if System.otp_release() |> String.to_integer() >= 26 do %{ "message" => _error_message2, "range" => %{ - "end" => %{"character" => 4, "line" => 2}, - "start" => %{"character" => 4, "line" => 2} + "end" => %{"line" => 2}, + "start" => %{"line" => 2} }, "severity" => 2, "source" => "ElixirLS Dialyzer" @@ -245,8 +245,8 @@ if System.otp_release() |> String.to_integer() >= 26 do %{ "message" => error_message1, "range" => %{ - "end" => %{"character" => 2, "line" => 1}, - "start" => %{"character" => 2, "line" => 1} + "end" => %{"line" => 1}, + "start" => %{"line" => 1} }, "severity" => 2, "source" => "ElixirLS Dialyzer" @@ -254,8 +254,8 @@ if System.otp_release() |> String.to_integer() >= 26 do %{ "message" => error_message2, "range" => %{ - "end" => %{"character" => 4, "line" => 2}, - "start" => %{"character" => 4, "line" => 2} + "end" => %{"line" => 2}, + "start" => %{"line" => 2} }, "severity" => 2, "source" => "ElixirLS Dialyzer" diff --git a/apps/language_server/test/dialyzer_test.exs b/apps/language_server/test/dialyzer_test.exs index 05ed22cee..f9cc7ffbf 100644 --- a/apps/language_server/test/dialyzer_test.exs +++ b/apps/language_server/test/dialyzer_test.exs @@ -73,8 +73,8 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do %{ "message" => error_message1, "range" => %{ - "end" => %{"character" => 2, "line" => 1}, - "start" => %{"character" => 2, "line" => 1} + "end" => %{"line" => 1}, + "start" => %{"line" => 1} }, "severity" => 2, "source" => "ElixirLS Dialyzer" @@ -82,8 +82,8 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do %{ "message" => error_message2, "range" => %{ - "end" => %{"character" => 4, "line" => 2}, - "start" => %{"character" => 4, "line" => 2} + "end" => %{"line" => 2}, + "start" => %{"line" => 2} }, "severity" => 2, "source" => "ElixirLS Dialyzer" @@ -189,8 +189,8 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do %{ "message" => error_message1, "range" => %{ - "end" => %{"character" => 2, "line" => 1}, - "start" => %{"character" => 2, "line" => 1} + "end" => %{"line" => 1}, + "start" => %{"line" => 1} }, "severity" => 2, "source" => "ElixirLS Dialyzer" @@ -198,8 +198,8 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do %{ "message" => error_message2, "range" => %{ - "end" => %{"character" => 4, "line" => 2}, - "start" => %{"character" => 4, "line" => 2} + "end" => %{"line" => 2}, + "start" => %{"line" => 2} }, "severity" => 2, "source" => "ElixirLS Dialyzer" @@ -239,8 +239,8 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do %{ "message" => error_message1, "range" => %{ - "end" => %{"character" => 2, "line" => 1}, - "start" => %{"character" => 2, "line" => 1} + "end" => %{"line" => 1}, + "start" => %{"line" => 1} }, "severity" => 2, "source" => "ElixirLS Dialyzer" @@ -248,8 +248,8 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do %{ "message" => error_message2, "range" => %{ - "end" => %{"character" => 4, "line" => 2}, - "start" => %{"character" => 4, "line" => 2} + "end" => %{"line" => 2}, + "start" => %{"line" => 2} }, "severity" => 2, "source" => "ElixirLS Dialyzer" @@ -279,8 +279,8 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do %{ "message" => error_message1, "range" => %{ - "end" => %{"character" => 2, "line" => 1}, - "start" => %{"character" => 2, "line" => 1} + "end" => %{"line" => 1}, + "start" => %{"line" => 1} }, "severity" => 2, "source" => "ElixirLS Dialyzer" @@ -288,8 +288,8 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do %{ "message" => _error_message2, "range" => %{ - "end" => %{"character" => 4, "line" => 2}, - "start" => %{"character" => 4, "line" => 2} + "end" => %{"line" => 2}, + "start" => %{"line" => 2} }, "severity" => 2, "source" => "ElixirLS Dialyzer" @@ -320,8 +320,8 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do %{ "message" => error_message1, "range" => %{ - "end" => %{"character" => 2, "line" => 1}, - "start" => %{"character" => 2, "line" => 1} + "end" => %{"line" => 1}, + "start" => %{"line" => 1} }, "severity" => 2, "source" => "ElixirLS Dialyzer" @@ -329,8 +329,8 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do %{ "message" => error_message2, "range" => %{ - "end" => %{"character" => 4, "line" => 2}, - "start" => %{"character" => 4, "line" => 2} + "end" => %{"line" => 2}, + "start" => %{"line" => 2} }, "severity" => 2, "source" => "ElixirLS Dialyzer" diff --git a/apps/language_server/test/providers/completion/suggestions_test.exs b/apps/language_server/test/providers/completion/suggestions_test.exs index 3b570b58b..b220a7791 100644 --- a/apps/language_server/test/providers/completion/suggestions_test.exs +++ b/apps/language_server/test/providers/completion/suggestions_test.exs @@ -637,7 +637,7 @@ defmodule ElixirLS.LanguageServer.Providers.Completion.SuggestionTest do type: :callback, metadata: %{optional: false, app: :language_server} } - ] == list + ] = list end test "lists macrocallbacks + def macros after defma" do @@ -4972,6 +4972,83 @@ defmodule ElixirLS.LanguageServer.Providers.Completion.SuggestionTest do ] = suggestions |> Enum.filter(&(&1.name == "user")) end + if Version.match?(System.version(), ">= 1.18.0") do + test "records fields" do + buffer = """ + defmodule SomeSchema do + require ElixirSenseExample.ModuleWithRecord, as: R + + def d() do + w = R.user() + w = R.user(n) + R.user(w, n) + R.user(w, name: "1", a) + end + end + """ + + suggestions = Suggestion.suggestions(buffer, 5, 16) + + assert [ + %{ + name: "age", + origin: "ElixirSenseExample.ModuleWithRecord.user", + type: :field, + call?: false, + subtype: :record_field, + type_spec: "integer()" + }, + %{ + name: "name", + origin: "ElixirSenseExample.ModuleWithRecord.user", + type: :field, + call?: false, + subtype: :record_field, + type_spec: "String.t()" + } + ] = suggestions |> Enum.filter(&(&1.type == :field)) + + suggestions = Suggestion.suggestions(buffer, 6, 17) + + assert [ + %{ + name: "name", + origin: "ElixirSenseExample.ModuleWithRecord.user", + type: :field, + call?: false, + subtype: :record_field, + type_spec: "String.t()" + } + ] = suggestions |> Enum.filter(&(&1.type == :field)) + + suggestions = Suggestion.suggestions(buffer, 7, 16) + + assert [ + %{ + name: "name", + origin: "ElixirSenseExample.ModuleWithRecord.user", + type: :field, + call?: false, + subtype: :record_field, + type_spec: "String.t()" + } + ] = suggestions |> Enum.filter(&(&1.type == :field)) + + suggestions = Suggestion.suggestions(buffer, 8, 27) + + assert [ + %{ + name: "age", + origin: "ElixirSenseExample.ModuleWithRecord.user", + type: :field, + call?: false, + subtype: :record_field, + type_spec: "integer()" + } + ] = suggestions |> Enum.filter(&(&1.type == :field)) + end + end + test "records from metadata fields" do buffer = """ defmodule SomeSchema do diff --git a/apps/language_server/test/providers/definition/locator_test.exs b/apps/language_server/test/providers/definition/locator_test.exs index a332f92db..080071a22 100644 --- a/apps/language_server/test/providers/definition/locator_test.exs +++ b/apps/language_server/test/providers/definition/locator_test.exs @@ -1132,29 +1132,30 @@ defmodule ElixirLS.LanguageServer.Providers.Definition.LocatorTest do } end - # TODO not supported in Code.Fragment.surround_context as of elixir 1.17 - # test "find definition of &1 capture variable" do - # buffer = """ - # defmodule MyModule do - # def go() do - # abc = 5 - # & [ - # &1, - # abc, - # cde = 1, - # record_env() - # ] - # end - # end - # """ - - # assert Locator.definition(buffer, 4, 8) == %Location{ - # type: :variable, - # file: nil, - # line: 4, - # column: 7 - # } - # end + if Version.match?(System.version(), ">= 1.18.0") do + test "find definition of &1 capture variable" do + buffer = """ + defmodule MyModule do + def go() do + abc = 5 + & [ + &1, + abc, + cde = 1, + record_env() + ] + end + end + """ + + assert %Location{ + type: :variable, + file: nil, + line: 5, + column: 7 + } = Locator.definition(buffer, 5, 8) + end + end test "find definition of write variable on definition" do buffer = """ @@ -1290,7 +1291,8 @@ defmodule ElixirLS.LanguageServer.Providers.Definition.LocatorTest do } # `a` redefined in a case clause - # TODO cursor lands in the wrong clause on 1.17 + # cursor lands in the wrong clause on 1.18 + # fortunately __cursor__ inserting hacks in ElixirSense.Metadata are able to work around this # defmodule MyModule do # def my_fun(a, b) do # case a do diff --git a/apps/language_server/test/providers/hover/docs_test.exs b/apps/language_server/test/providers/hover/docs_test.exs index cf75f5821..eb0f84484 100644 --- a/apps/language_server/test/providers/hover/docs_test.exs +++ b/apps/language_server/test/providers/hover/docs_test.exs @@ -26,12 +26,12 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do docs: [doc] } = Docs.docs("ElixirSenseExample.ModuleWithDocFalse", 1, 22) - assert doc == %{ + assert %{ module: ElixirSenseExample.ModuleWithDocFalse, metadata: %{hidden: true, app: :language_server}, docs: "", kind: :module - } + } = doc end test "module with no @moduledoc" do @@ -39,12 +39,12 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do docs: [doc] } = Docs.docs("ElixirSenseExample.ModuleWithNoDocs", 1, 22) - assert doc == %{ + assert %{ module: ElixirSenseExample.ModuleWithNoDocs, metadata: %{app: :language_server}, docs: "", kind: :module - } + } = doc end test "retrieve documentation from modules" do @@ -59,7 +59,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do } = Docs.docs(buffer, 2, 8) assert doc.module == GenServer - assert doc.metadata == %{app: :elixir} + assert %{app: :elixir} = doc.metadata assert doc.kind == :module assert doc.docs =~ """ @@ -85,12 +85,12 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do docs: [doc] } = Docs.docs(buffer, 9, 15) - assert doc == %{ + assert %{ module: MyLocalModule, metadata: %{since: "1.2.3"}, docs: "Some example doc", kind: :module - } + } = doc end test "retrieve documentation from metadata modules on __MODULE__" do @@ -109,12 +109,12 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do docs: [doc] } = Docs.docs(buffer, 6, 6) - assert doc == %{ + assert %{ module: MyLocalModule, metadata: %{since: "1.2.3"}, docs: "Some example doc", kind: :module - } + } = doc end if Version.match?(System.version(), ">= 1.14.0") do @@ -136,12 +136,12 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do docs: [doc] } = Docs.docs(buffer, 8, 17) - assert doc == %{ + assert %{ module: MyLocalModule.Sub, metadata: %{since: "1.2.3"}, docs: "Some example doc", kind: :module - } + } = doc end end @@ -162,7 +162,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do docs: [doc] } = Docs.docs(buffer, 8, 15) - assert doc == %{module: MyLocalModule, metadata: %{hidden: true}, docs: "", kind: :module} + assert %{module: MyLocalModule, metadata: %{hidden: true}, docs: "", kind: :module} = doc end test "retrieve documentation from erlang modules" do @@ -278,7 +278,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do docs: [doc] } = Docs.docs("ElixirSenseExample.ModuleWithDocs.some_fun_doc_false(1)", 1, 40) - assert doc == %{ + assert %{ module: ElixirSenseExample.ModuleWithDocs, metadata: %{hidden: true, defaults: 1, app: :language_server}, docs: "", @@ -287,7 +287,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do arity: 2, function: :some_fun_doc_false, specs: [] - } + } = doc end test "function no @doc" do @@ -295,7 +295,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do docs: [doc] } = Docs.docs("ElixirSenseExample.ModuleWithDocs.some_fun_no_doc(1)", 1, 40) - assert doc == %{ + assert %{ docs: "", kind: :function, metadata: %{defaults: 1, app: :language_server}, @@ -304,7 +304,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do arity: 2, function: :some_fun_no_doc, specs: [] - } + } = doc end test "retrieve function documentation" do @@ -640,7 +640,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do docs: [doc] } = Docs.docs(buffer, 12, 20) - assert doc == %{ + assert %{ args: ["list"], function: :flatten, arity: 1, @@ -654,7 +654,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do module: MyLocalModule, specs: ["@callback flatten(list()) :: list()"], docs: "Sample doc" - } + } = doc end test "retrieve metadata function documentation - fallback to callback no @impl" do @@ -678,7 +678,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do docs: [doc] } = Docs.docs(buffer, 11, 20) - assert doc == %{ + assert %{ args: ["list"], function: :flatten, arity: 1, @@ -691,7 +691,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do module: MyLocalModule, specs: ["@callback flatten(list()) :: list()"], docs: "Sample doc" - } + } = doc end test "retrieve metadata function documentation - fallback to erlang callback" do @@ -758,7 +758,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do docs: [doc] } = Docs.docs(buffer, 13, 20) - assert doc == %{ + assert %{ args: ["list"], arity: 1, function: :bar, @@ -772,7 +772,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do specs: ["@macrocallback bar(integer()) :: Macro.t()"], docs: "Docs for bar", kind: :macro - } + } = doc end test "retrieve local private metadata function documentation on __MODULE__ call" do @@ -1017,7 +1017,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do docs: [doc] } = Docs.docs(buffer, 3, 12) - assert doc == %{ + assert %{ args: ["var"], function: :some, arity: 1, @@ -1028,7 +1028,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do ], docs: "some macro\n", kind: :macro - } + } = doc end if Version.match?(System.version(), ">= 1.14.0") do @@ -1206,7 +1206,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do docs: [doc] } = Docs.docs(buffer, 3, 5) - assert doc == %{ + assert %{ args: [], function: :foo, arity: 0, @@ -1219,7 +1219,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do specs: ["@callback foo() :: :ok"], docs: "Docs for foo", kind: :function - } + } = doc end test "retrieve function documentation from behaviour even if @doc is set to false vie @impl" do @@ -1234,7 +1234,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do docs: [doc] } = Docs.docs(buffer, 3, 5) - assert doc == %{ + assert %{ args: ["a"], function: :baz, arity: 1, @@ -1248,7 +1248,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do }, docs: "Docs for baz", kind: :function - } + } = doc end test "retrieve function documentation from behaviour when callback has @doc false" do @@ -1263,7 +1263,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do docs: [doc] } = Docs.docs(buffer, 3, 5) - assert doc == %{ + assert %{ args: [], function: :foo, arity: 0, @@ -1277,7 +1277,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do specs: ["@callback foo() :: :ok"], docs: "", kind: :function - } + } = doc end test "retrieve macro documentation from behaviour if available" do @@ -1292,7 +1292,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do docs: [doc] } = Docs.docs(buffer, 3, 5) - assert doc == %{ + assert %{ args: ["b"], arity: 1, function: :bar, @@ -1305,7 +1305,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do specs: ["@macrocallback bar(integer()) :: Macro.t()"], docs: "Docs for bar", kind: :macro - } + } = doc end if System.otp_release() |> String.to_integer() >= 25 do @@ -1375,7 +1375,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do docs: [doc] } = Docs.docs(buffer, 3, 31) - assert doc == %{ + assert %{ args: [], arity: 0, docs: "", @@ -1384,7 +1384,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do module: ElixirSenseExample.ModuleWithDocs, spec: "@type some_type_doc_false() :: integer()", type: :some_type_doc_false - } + } = doc end test "type no @typedoc" do @@ -1400,7 +1400,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do docs: [doc] } = Docs.docs(buffer, 3, 31) - assert doc == %{ + assert %{ args: [], arity: 0, docs: "", @@ -1409,7 +1409,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do module: ElixirSenseExample.ModuleWithDocs, spec: "@type some_type_no_doc() :: integer()", type: :some_type_no_doc - } + } = doc end test "retrieve type documentation" do @@ -1425,7 +1425,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do docs: [doc] } = Docs.docs(buffer, 3, 31) - assert doc == %{ + assert %{ args: [], arity: 0, docs: "Remote type", @@ -1434,7 +1434,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do module: ElixirSenseExample.ModuleWithTypespecs.Remote, spec: "@type remote_t() :: atom()", type: :remote_t - } + } = doc end test "retrieve metadata type documentation" do @@ -1598,7 +1598,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do docs: [doc] } = Docs.docs(buffer, 3, 35) - assert doc == %{ + assert %{ args: ["x"], type: :t, arity: 1, @@ -1607,7 +1607,7 @@ defmodule ElixirLS.LanguageServer.Providers.Hover.DocsTest do metadata: %{opaque: true, app: :language_server}, docs: "Opaque type\n", kind: :type - } + } = doc end test "retrieve erlang type documentation" do diff --git a/apps/language_server/test/providers/references/locator_test.exs b/apps/language_server/test/providers/references/locator_test.exs index 57d9117ac..c61398e71 100644 --- a/apps/language_server/test/providers/references/locator_test.exs +++ b/apps/language_server/test/providers/references/locator_test.exs @@ -411,7 +411,7 @@ defmodule ElixirLS.LanguageServer.Providers.References.LocatorTest do } ] = references - if Version.match?(System.version(), ">= 1.14.0-rc.0") do + if Version.match?(System.version(), ">= 1.14.0") do # before 1.14 tracer reports invalid positions for captures # https://github.com/elixir-lang/elixir/issues/12023 assert range == %{start: %{line: 55, column: 72}, end: %{line: 55, column: 83}} diff --git a/apps/language_server/test/providers/selection_ranges_test.exs b/apps/language_server/test/providers/selection_ranges_test.exs index f2d73df26..fa824c8ba 100644 --- a/apps/language_server/test/providers/selection_ranges_test.exs +++ b/apps/language_server/test/providers/selection_ranges_test.exs @@ -228,7 +228,7 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do assert_range(ranges, range(1, 0, 2, 4)) end - if Version.match?(System.version(), ">= 1.14.0-dev") do + if Version.match?(System.version(), ">= 1.14.0") do test "left from do" do text = """ do @@ -262,7 +262,7 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do assert_range(ranges, range(0, 0, 3, 3)) end - if Version.match?(System.version(), ">= 1.14.0-dev") do + if Version.match?(System.version(), ">= 1.14.0") do test "left from end" do text = """ do @@ -767,6 +767,12 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do assert_range(ranges, range(0, 0, 1, 0)) # full for assert_range(ranges, range(0, 0, 0, 56)) + + if Version.match?(System.version(), ">= 1.18.0") do + # do: x + y expression + assert_range(ranges, range(0, 47, 0, 56)) + end + # x + y expression assert_range(ranges, range(0, 51, 0, 56)) end @@ -994,6 +1000,14 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do assert_range(ranges, range(0, 0, 3, 1)) # full keyword assert_range(ranges, range(0, 6, 2, 6)) + + if Version.match?(System.version(), ">= 1.18.0") do + # key and value + assert_range(ranges, range(1, 2, 1, 6)) + + # key + assert_range(ranges, range(1, 2, 1, 4)) + end end end @@ -1030,7 +1044,7 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do assert_range(ranges, range(0, 8, 0, 12)) end - if Version.match?(System.version(), ">= 1.14.0-dev") do + if Version.match?(System.version(), ">= 1.14.0") do test "left side of | near" do text = """ %{state | 1 => 1, counter: counter + to_dispatch, demand: demand - to_dispatch} @@ -1062,4 +1076,31 @@ defmodule ElixirLS.LanguageServer.Providers.SelectionRangesTest do assert_range(ranges, range(0, 2, 0, 78)) end end + + test "expression with parens" do + text = """ + 1 + (2 * (3 + (s + x)) / 1) + """ + + ranges = get_ranges(text, 0, 15) + + # full range + assert_range(ranges, range(0, 0, 1, 0)) + # full line + assert_range(ranges, range(0, 0, 0, 27)) + # outside outermost parens + assert_range(ranges, range(0, 4, 0, 27)) + # inside outermost parens + assert_range(ranges, range(0, 5, 0, 26)) + # outside middle parens + assert_range(ranges, range(0, 9, 0, 22)) + # inside middle parens + assert_range(ranges, range(0, 10, 0, 21)) + # outside innermost parens + assert_range(ranges, range(0, 14, 0, 21)) + # inside innermost parens + assert_range(ranges, range(0, 15, 0, 20)) + # s variable + assert_range(ranges, range(0, 15, 0, 16)) + end end diff --git a/apps/language_server/test/providers/signature_help/signature_test.exs b/apps/language_server/test/providers/signature_help/signature_test.exs index aabf1843b..918585b0e 100644 --- a/apps/language_server/test/providers/signature_help/signature_test.exs +++ b/apps/language_server/test/providers/signature_help/signature_test.exs @@ -697,7 +697,7 @@ defmodule ElixirLS.LanguageServer.Providers.SignatureHelp.SignatureTest do end """ - assert Signature.signature(code, 2, 14) == %{ + assert %{ active_param: 1, signatures: [ %{ @@ -705,7 +705,7 @@ defmodule ElixirLS.LanguageServer.Providers.SignatureHelp.SignatureTest do params: ["fun", "args"], documentation: "Invokes the given anonymous function `fun` with the list of\narguments `args`.", - spec: "@spec apply((... -> any()), [any()]) :: any()" + spec: "@spec apply(" <> _ }, %{ name: "apply", @@ -715,7 +715,7 @@ defmodule ElixirLS.LanguageServer.Providers.SignatureHelp.SignatureTest do spec: "@spec apply(module(), function_name :: atom(), [any()]) :: any()" } ] - } + } = Signature.signature(code, 2, 14) end test "finds signatures from local functions" do @@ -1392,7 +1392,16 @@ defmodule ElixirLS.LanguageServer.Providers.SignatureHelp.SignatureTest do end """ - assert Signature.signature(code, 2, 8) == :none + if Version.match?(System.version(), "< 1.18.0") do + assert Signature.signature(code, 2, 8) == :none + else + assert %{ + signatures: [ + %{name: "defmodule"} + ], + active_param: 1 + } = Signature.signature(code, 2, 8) + end end test "return :none when no signature is found" do diff --git a/apps/language_server/test/support/module_with_typespecs.ex b/apps/language_server/test/support/module_with_typespecs.ex index 94ffd2772..0f2592a53 100644 --- a/apps/language_server/test/support/module_with_typespecs.ex +++ b/apps/language_server/test/support/module_with_typespecs.ex @@ -191,13 +191,13 @@ defmodule ElixirSenseExample.ModuleWithTypespecs do end @spec fun_with_default(atom, [{:foo, integer()} | {:bar, String.t()}]) :: :ok - def fun_with_default(a \\ nil, options), do: :ok + def fun_with_default(_a \\ nil, _options), do: :ok @spec multiple_functions([{:foo, integer()}]) :: :ok - def multiple_functions(options), do: :ok + def multiple_functions(_options), do: :ok @spec multiple_functions([{:bar, String.t()}]) :: :ok - def multiple_functions(options, a), do: :ok + def multiple_functions(_options, _a), do: :ok end defmodule Behaviour do @@ -206,7 +206,7 @@ defmodule ElixirSenseExample.ModuleWithTypespecs do defmodule Impl do @behaviour Behaviour - def some(a), do: :ok + def some(_a), do: :ok end defmodule MacroBehaviour do @@ -215,6 +215,6 @@ defmodule ElixirSenseExample.ModuleWithTypespecs do defmodule MacroImpl do @behaviour MacroBehaviour - defmacro some(a), do: :ok + defmacro some(_a), do: :ok end end diff --git a/apps/language_server/test/support/modules_with_references.ex b/apps/language_server/test/support/modules_with_references.ex index c030212e5..e4097848e 100644 --- a/apps/language_server/test/support/modules_with_references.ex +++ b/apps/language_server/test/support/modules_with_references.ex @@ -97,7 +97,7 @@ defmodule ElixirSense.Providers.ReferencesTest.Modules do defmodule Caller6 do def func() do - ElixirSense.Providers.ReferencesTest.Modules.Callee6.__info__(:function) + ElixirSense.Providers.ReferencesTest.Modules.Callee6.__info__(:functions) ElixirSense.Providers.ReferencesTest.Modules.Callee6.module_info() end end diff --git a/apps/language_server/test/support/server_test_helpers.ex b/apps/language_server/test/support/server_test_helpers.ex index 334b9f8b6..745aa7785 100644 --- a/apps/language_server/test/support/server_test_helpers.ex +++ b/apps/language_server/test/support/server_test_helpers.ex @@ -27,7 +27,7 @@ defmodule ElixirLS.LanguageServer.Test.ServerTestHelpers do def replace_logger(packet_capture) do # :logger application is already started # replace console logger with LSP - if Version.match?(System.version(), ">= 1.15.0-dev") do + if Version.match?(System.version(), ">= 1.15.0") do configs = for handler_id <- :logger.get_handler_ids() do {:ok, config} = :logger.get_handler_config(handler_id) diff --git a/dep_versions.exs b/dep_versions.exs index f68f4795b..dfb968874 100644 --- a/dep_versions.exs +++ b/dep_versions.exs @@ -1,5 +1,5 @@ [ - elixir_sense: "99db60361b34cb952a767461dd46ed455d15e4cb", + elixir_sense: "809a0924b40562d3c779e481a3c8970a52640c21", dialyxir_vendored: "f8f64cfb6797c518294687e7c03ae817bacbc6ee", jason_v: "f1c10fa9c445cb9f300266122ef18671054b2330", erl2ex_vendored: "073ac6b9a44282e718b6050c7b27cedf9217a12a", diff --git a/mix.lock b/mix.lock index f3aa2ba37..256f5f2e6 100644 --- a/mix.lock +++ b/mix.lock @@ -2,7 +2,7 @@ "benchee": {:hex, :benchee, "1.1.0", "f3a43817209a92a1fade36ef36b86e1052627fd8934a8b937ac9ab3a76c43062", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}], "hexpm", "7da57d545003165a012b587077f6ba90b89210fd88074ce3c60ce239eb5e6d93"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "dialyxir_vendored": {:git, "https://github.com/elixir-lsp/dialyxir.git", "f8f64cfb6797c518294687e7c03ae817bacbc6ee", [ref: "f8f64cfb6797c518294687e7c03ae817bacbc6ee"]}, - "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "99db60361b34cb952a767461dd46ed455d15e4cb", [ref: "99db60361b34cb952a767461dd46ed455d15e4cb"]}, + "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "809a0924b40562d3c779e481a3c8970a52640c21", [ref: "809a0924b40562d3c779e481a3c8970a52640c21"]}, "erl2ex_vendored": {:git, "https://github.com/elixir-lsp/erl2ex.git", "073ac6b9a44282e718b6050c7b27cedf9217a12a", [ref: "073ac6b9a44282e718b6050c7b27cedf9217a12a"]}, "erlex_vendored": {:git, "https://github.com/elixir-lsp/erlex.git", "c0e448db27bcbb3f369861d13e3b0607ed37048d", [ref: "c0e448db27bcbb3f369861d13e3b0607ed37048d"]}, "jason_v": {:git, "https://github.com/elixir-lsp/jason.git", "f1c10fa9c445cb9f300266122ef18671054b2330", [ref: "f1c10fa9c445cb9f300266122ef18671054b2330"]},