Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
### Unreleased
- Added option `elixirLS.dotFormatter` to specify path to custom `.formatter.exs`

### v0.28.1: 24 May 2025

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,7 @@ Below is a list of configuration options supported by the ElixirLS language serv
<dt>elixirLS.additionalWatchedExtensions</dt><dd>Additional file types capable of triggering a build on change</dd>
<dt>elixirLS.languageServerOverridePath</dt><dd>Absolute path to an alternative ElixirLS release that will override the packaged release</dd>
<dt>elixirLS.stdlibSrcDir</dt><dd>Path to Elixir's std lib source code. See [here](https://github.com/elixir-lsp/elixir_sense/pull/277) for more info</dd>
<dt>elixirLS.dotFormatter</dt><dd>Path to a custom <code>.formatter.exs</code> file used when formatting documents</dd>
</dl>

## Debug Adapter configuration options
Expand Down
24 changes: 16 additions & 8 deletions apps/language_server/lib/language_server/providers/formatting.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand Down
9 changes: 8 additions & 1 deletion apps/language_server/lib/language_server/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
33 changes: 24 additions & 9 deletions apps/language_server/lib/language_server/source_file.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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 ->
Expand All @@ -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
Expand Down
20 changes: 20 additions & 0 deletions apps/language_server/test/providers/formatting_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading