How to make whitespace changes via the AST with mix format?

I’m trying to write a couple of mix format plugins for my project. So far I’ve been able to achieve most of what I want (e.g. rewriting function calls) via manipulating the AST except for one that I’m trying to write that involves comments and whitespace modifications.

Specifically, I’d like this plugin to be able to insert blank lines at arbitrary points in the file it’s formatting. For example, I’d like it to be able to insert a comment or then insert some number of blank lines between that comment and the module definition or to insert blank lines between function definitions.

defmodule MyModule do
...

would become

# This is my comment

defmodule MyModule do

and

def function_one(foo) do
...
end
def function_two(bar) do
...
end

would become

def function_one(foo) do
...
end

def function_two(bar) do
...
end

Is this possible through manipulating the AST? And if so, how?

So far I haven’t been able to by changing the line metadata of the AST nodes. However, reading through some of the Styler codebase they are at least manipulating multiline functions through the metadata. To me that seems essentially the same so it seems like I am missing something.

However, from my testing it also seems like information about whitespace gets lost/ignored in the translation to/from AST. For example, this is an AST from my plugin generated from Code.string_to_quoted_with_comments!/2 and despite changing all the line metadata this information is lost going from AST back to string (to be written to the file):

iex(1)> new_ast = {:defmodule, [do: [line: 36], end: [line: 40], line: 36], [{:__aliases__, [last: [line: 36], line: 36], [:PlaygroundSimple]}, [{{:__block__, [line: 36], [:do]}, {:def, [do: [line: 37], end: [line: 39], line: 37], [{:test, [line: 37], nil}, [{{:__block__, [line: 37], [:do]}, {:__block__, [delimiter: "\"", line: 38], ["Hello, World"]}}]]}}]]}
{:defmodule, [do: [line: 36], end: [line: 40], line: 36],
 [
   {:__aliases__, [last: [line: 36], line: 36], [:PlaygroundSimple]},
   [
     {{:__block__, [line: 36], [:do]},
      {:def, [do: [line: 37], end: [line: 39], line: 37],
       [
         {:test, [line: 37], nil},
         [
           {{:__block__, [line: 37], [:do]},
            {:__block__, [delimiter: "\"", line: 38], ["Hello, World"]}}
         ]
       ]}}
   ]
 ]}

iex(3)> Code.quoted_to_algebra(new_ast) |> Inspect.Algebra.format(120)
["defmodule", " ", "PlaygroundSimple", " do", "\n  ", "def", " ", "test", " do",
 "\n    ", "\"", "Hello, World", "\"", "\n  ", "end", "\n", "end"]

If that is the case is there a way around the AST not being able to manipulate whitespace that is recommended? Using regex/string manipulation seems like an alternative but not a particularly robust one.

hexdocs.pm/sourceror