diff --git a/CHANGELOG.md b/CHANGELOG.md index ee6401273..6f40553ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ ### Unreleased +- Added option `elixirLS.dotFormatter` to specify path to custom `.formatter.exs` ### v0.28.1: 24 May 2025 diff --git a/README.md b/README.md index 57789cbcc..f7922cffc 100644 --- a/README.md +++ b/README.md @@ -486,6 +486,7 @@ Below is a list of configuration options supported by the ElixirLS language serv
elixirLS.additionalWatchedExtensions
Additional file types capable of triggering a build on change
elixirLS.languageServerOverridePath
Absolute path to an alternative ElixirLS release that will override the packaged release
elixirLS.stdlibSrcDir
Path to Elixir's std lib source code. See [here](https://github.com/elixir-lsp/elixir_sense/pull/277) for more info
+
elixirLS.dotFormatter
Path to a custom .formatter.exs file used when formatting documents
## Debug Adapter configuration options diff --git a/apps/language_server/lib/language_server/providers/formatting.ex b/apps/language_server/lib/language_server/providers/formatting.ex index b8de0e477..86c3c291a 100644 --- a/apps/language_server/lib/language_server/providers/formatting.ex +++ b/apps/language_server/lib/language_server/providers/formatting.ex @@ -4,18 +4,26 @@ defmodule ElixirLS.LanguageServer.Providers.Formatting do import ElixirLS.LanguageServer.RangeUtils require Logger - def format(%SourceFile{} = source_file, uri = "file:" <> _, project_dir, mix_project?) + def format(source_file, uri, project_dir, mix_project?, opts \\ []) + + def format( + %SourceFile{} = source_file, + uri = "file:" <> _, + project_dir, + mix_project?, + opts + ) when is_binary(project_dir) do file_path = SourceFile.Path.absolute_from_uri(uri, project_dir) # file_path and project_dir are absolute paths with universal separators 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 = opts[:root] + case SourceFile.formatter_for(uri, project_dir, mix_project?, opts) do + {:ok, {formatter, formatter_opts}} -> + formatter_exs_dir = formatter_opts[:root] - if should_format?(uri, formatter_exs_dir, opts[:inputs], project_dir) do - do_format(source_file, formatter, opts) + if should_format?(uri, formatter_exs_dir, formatter_opts[:inputs], project_dir) do + do_format(source_file, formatter, formatter_opts) else JsonRpc.show_message( :info, @@ -47,8 +55,8 @@ defmodule ElixirLS.LanguageServer.Providers.Formatting do end end - # if project_dir is not set or schema is not file we format with default options - def format(%SourceFile{} = source_file, _uri, _project_dir, _mix_project?) do + # if project_dir is not set or scheme is not file we format with default options + def format(%SourceFile{} = source_file, _uri, _project_dir, _mix_project?, _opts) do do_format(source_file, nil, []) end diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index d65799874..94cceb3d6 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -1535,7 +1535,14 @@ defmodule ElixirLS.LanguageServer.Server do state = %__MODULE__{} ) do source_file = get_source_file(state, uri) - fun = fn -> Formatting.format(source_file, uri, state.project_dir, state.mix_project?) end + dot_formatter = Map.get(state.settings || %{}, "dotFormatter") + + fun = fn -> + Formatting.format(source_file, uri, state.project_dir, state.mix_project?, + dot_formatter: dot_formatter + ) + end + {:async, fun, state} end diff --git a/apps/language_server/lib/language_server/source_file.ex b/apps/language_server/lib/language_server/source_file.ex index 67c8f2a09..3905c64f1 100644 --- a/apps/language_server/lib/language_server/source_file.ex +++ b/apps/language_server/lib/language_server/source_file.ex @@ -235,9 +235,12 @@ defmodule ElixirLS.LanguageServer.SourceFile do """ end - @spec formatter_for(String.t(), String.t() | nil, boolean) :: + @spec formatter_for(String.t(), String.t() | nil, boolean, keyword()) :: {:ok, {function | nil, keyword()}} | {:error, any} - def formatter_for(uri = "file:" <> _, project_dir, mix_project?) when is_binary(project_dir) do + def formatter_for(uri, project_dir, mix_project?, opts \\ []) + + def formatter_for(uri = "file:" <> _, project_dir, mix_project?, opts) + when is_binary(project_dir) do path = __MODULE__.Path.from_uri(uri) try do @@ -250,7 +253,7 @@ defmodule ElixirLS.LanguageServer.SourceFile do {:ok, config_mtime} = MixProjectCache.config_mtime() {:ok, mix_project} = MixProjectCache.get() - opts = [ + formatter_opts = [ deps_paths: deps_paths, manifest_path: manifest_path, config_mtime: config_mtime, @@ -286,16 +289,20 @@ defmodule ElixirLS.LanguageServer.SourceFile do end ] - {:ok, Mix.Tasks.ElixirLSFormat.formatter_for_file(path, opts)} + formatter_opts = + formatter_opts + |> maybe_put_dot_formatter(opts) + + {:ok, Mix.Tasks.ElixirLSFormat.formatter_for_file(path, formatter_opts)} else {:error, :project_not_loaded} end else - opts = [ - root: project_dir - ] + formatter_opts = + [root: project_dir] + |> maybe_put_dot_formatter(opts) - {:ok, Mix.Tasks.ElixirLSFormat.formatter_for_file(path, opts)} + {:ok, Mix.Tasks.ElixirLSFormat.formatter_for_file(path, formatter_opts)} end catch kind, payload -> @@ -322,7 +329,15 @@ defmodule ElixirLS.LanguageServer.SourceFile do end end - def formatter_for(_, _, _), do: {:error, :project_dir_not_set} + def formatter_for(_, _, _, _), do: {:error, :project_dir_not_set} + + defp maybe_put_dot_formatter(opts_list, opts) do + if dot = Keyword.get(opts, :dot_formatter) do + Keyword.put(opts_list, :dot_formatter, dot) + else + opts_list + end + end defp format_code(code, opts) do try do diff --git a/apps/language_server/test/providers/formatting_test.exs b/apps/language_server/test/providers/formatting_test.exs index d56dc7b22..e2c582573 100644 --- a/apps/language_server/test/providers/formatting_test.exs +++ b/apps/language_server/test/providers/formatting_test.exs @@ -503,4 +503,24 @@ defmodule ElixirLS.LanguageServer.Providers.FormattingTest do MixProjectCache.store(state) end + + @tag :fixture + test "custom dot formatter path is used" do + in_fixture(Path.join(__DIR__, ".."), "formatter", fn -> + store_mix_cache() + project_dir = Path.expand(".") + path = Path.join(project_dir, "lib/custom.ex") + File.write!(path, "foo 1") + source_file = %SourceFile{text: "foo 1", version: 1, dirty?: true} + uri = SourceFile.Path.to_uri(path) + + assert {:ok, [%TextEdit{}, %TextEdit{}]} = + Formatting.format(source_file, uri, project_dir, true) + + assert {:ok, []} = + Formatting.format(source_file, uri, project_dir, true, + dot_formatter: Path.join(project_dir, "lib/.formatter.exs") + ) + end) + end end