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.additionalWatchedExtensionsAdditional file types capable of triggering a build on change
elixirLS.languageServerOverridePathAbsolute path to an alternative ElixirLS release that will override the packaged release
elixirLS.stdlibSrcDirPath to Elixir's std lib source code. See [here](https://github.com/elixir-lsp/elixir_sense/pull/277) for more info
+elixirLS.dotFormatterPath 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