From aa33037af25995aa598cf533c00c4b76ae872d8e Mon Sep 17 00:00:00 2001 From: Aaron Ross Date: Wed, 15 Apr 2026 13:14:42 -0700 Subject: [PATCH 1/2] format sigils using applicable plugins See https://github.com/elixir-lang/elixir/blob/d67cb9634baba80e61188da5a688c9b127f94038/lib/mix/lib/mix/tasks/format.ex#L337 for corresponding implementation in `mix format`. --- internal/lsp/formatter_server.exs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/internal/lsp/formatter_server.exs b/internal/lsp/formatter_server.exs index 12de0f1..a9574c2 100644 --- a/internal/lsp/formatter_server.exs +++ b/internal/lsp/formatter_server.exs @@ -168,6 +168,25 @@ defmodule Formatter.Loop do Process.group_leader(self(), old_gl) end else + sigils = + for plugin <- plugins, + sigil <- List.wrap(plugin.features(format_opts)[:sigils]), + do: {sigil, plugin} + + sigils = + sigils + |> Enum.group_by(&elem(&1, 0), &elem(&1, 1)) + |> Enum.map(fn {sigil, plugins} -> + {sigil, + fn input, plugin_opts -> + Enum.reduce(plugins, input, fn plugin, acc -> + plugin.format(acc, plugin_opts ++ opts) + end) + end} + end) + + opts = Keyword.put(opts, :sigils, sigils) + content |> Code.format_string!(opts) |> IO.iodata_to_binary() end From 5313c05f676b07a5b5672ecc1c64ead1db7e1077 Mon Sep 17 00:00:00 2001 From: Aaron Ross Date: Thu, 16 Apr 2026 10:08:02 -0700 Subject: [PATCH 2/2] wrap all `plugin.format` calls in `try` This should avoid polluting stdout with any exception messages. --- internal/lsp/formatter_server.exs | 81 ++++++++++++++++++------------- 1 file changed, 47 insertions(+), 34 deletions(-) diff --git a/internal/lsp/formatter_server.exs b/internal/lsp/formatter_server.exs index a9574c2..52becb6 100644 --- a/internal/lsp/formatter_server.exs +++ b/internal/lsp/formatter_server.exs @@ -144,50 +144,63 @@ defmodule Formatter.Loop do opts = if filename != "", do: [file: filename] ++ format_opts, else: format_opts ext = Path.extname(filename) - # Filter plugins to those that handle this file extension - applicable_plugins = - Enum.filter(plugins, fn plugin -> + # Find plugins that handle this file extension or support .ex / .exs sigils + {applicable_plugins, sigils} = + Enum.reduce(plugins, {[], []}, fn plugin, {acc_plugins, acc_sigils} -> features = plugin.features(format_opts) extensions = Keyword.get(features, :extensions, []) + # If a plugin declares no extensions, it handles .ex/.exs by default - extensions == [] or ext in extensions + acc_plugins = + if extensions == [] or ext in extensions do + acc_plugins ++ [plugin] + else + acc_plugins + end + + sigils = + features + |> Keyword.get(:sigils) + |> List.wrap() + |> Enum.map(fn sigil -> {sigil, plugin} end) + + {acc_plugins, acc_sigils ++ sigils} end) - formatted = - if applicable_plugins != [] do - # Redirect group leader to stderr during plugin calls so any - # IO.puts from plugins doesn't corrupt the binary protocol on stdout. - old_gl = Process.group_leader() - Process.group_leader(self(), Process.whereis(:standard_error)) + # Redirect group leader to stderr during plugin calls so any + # IO.puts from plugins doesn't corrupt the binary protocol on stdout. + old_gl = Process.group_leader() + Process.group_leader(self(), Process.whereis(:standard_error)) - try do + formatted = + try do + if applicable_plugins != [] do Enum.reduce(applicable_plugins, content, fn plugin, acc -> plugin.format(acc, opts) end) - after - Process.group_leader(self(), old_gl) + else + sigils = + sigils + |> Enum.group_by(&elem(&1, 0), &elem(&1, 1)) + |> Enum.map(fn {sigil, plugins} -> + {sigil, + fn input, sigil_opts -> + Enum.reduce(plugins, input, fn plugin, acc -> + plugin.format(acc, sigil_opts ++ opts) + end) + end} + end) + + # `Code.format_string!/2` supports an undocumented `sigils` option + # that is passed to the underlying `quoted_to_algebra` and can transform + # the contents of a sigil during formatting. See available options in + # `Code.Formatter.maybe_sigil_to_algebra/4`. + opts = Keyword.put(opts, :sigils, sigils) + + content |> Code.format_string!(opts) |> IO.iodata_to_binary() end - else - sigils = - for plugin <- plugins, - sigil <- List.wrap(plugin.features(format_opts)[:sigils]), - do: {sigil, plugin} - - sigils = - sigils - |> Enum.group_by(&elem(&1, 0), &elem(&1, 1)) - |> Enum.map(fn {sigil, plugins} -> - {sigil, - fn input, plugin_opts -> - Enum.reduce(plugins, input, fn plugin, acc -> - plugin.format(acc, plugin_opts ++ opts) - end) - end} - end) - - opts = Keyword.put(opts, :sigils, sigils) - - content |> Code.format_string!(opts) |> IO.iodata_to_binary() + after + Process.group_leader(self(), old_gl) end # Ensure trailing newline to match mix format output