From 71bcf43132adca65fc28aafaa9fbb104985a7780 Mon Sep 17 00:00:00 2001
From: Mitchell Hanberg <mitch@mitchellhanberg.com>
Date: Mon, 19 Jun 2023 15:55:38 -0700
Subject: [PATCH] feat: formatting

---
 lib/next_ls.ex         | 48 +++++++++++++++++++++++++----
 lib/next_ls/runtime.ex |  4 +--
 test/next_ls_test.exs  | 68 ++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 112 insertions(+), 8 deletions(-)

diff --git a/lib/next_ls.ex b/lib/next_ls.ex
index b93a5c18..d0f70ebe 100644
--- a/lib/next_ls.ex
+++ b/lib/next_ls.ex
@@ -17,16 +17,23 @@ defmodule NextLS do
     TextDocumentDidSave
   }
 
-  alias GenLSP.Requests.{Initialize, Shutdown}
+  alias GenLSP.Requests.{
+    Initialize,
+    Shutdown,
+    TextDocumentFormatting
+  }
 
   alias GenLSP.Structures.{
     DidOpenTextDocumentParams,
     InitializeParams,
     InitializeResult,
+    Position,
+    Range,
     SaveOptions,
     ServerCapabilities,
     TextDocumentItem,
-    TextDocumentSyncOptions
+    TextDocumentSyncOptions,
+    TextEdit
   }
 
   alias NextLS.Runtime
@@ -80,12 +87,35 @@ defmodule NextLS do
            open_close: true,
            save: %SaveOptions{include_text: true},
            change: TextDocumentSyncKind.full()
-         }
+         },
+         document_formatting_provider: true
        },
        server_info: %{name: "NextLS"}
      }, assign(lsp, root_uri: root_uri)}
   end
 
+  def handle_request(%TextDocumentFormatting{params: %{text_document: %{uri: uri}}}, lsp) do
+    document = lsp.assigns.documents[uri]
+
+    working_dir = URI.parse(lsp.assigns.root_uri).path
+    {opts, _} = Code.eval_file(".formatter.exs", working_dir)
+    new_document = Code.format_string!(Enum.join(document, "\n"), opts) |> IO.iodata_to_binary()
+
+    {:reply,
+     [
+       %TextEdit{
+         new_text: new_document,
+         range: %Range{
+           start: %Position{line: 0, character: 0},
+           end: %Position{
+             line: length(document),
+             character: document |> List.last() |> String.length() |> Kernel.-(1) |> max(0)
+           }
+         }
+       }
+     ], lsp}
+  end
+
   def handle_request(%Shutdown{}, lsp) do
     {:reply, nil, assign(lsp, exit_code: 0)}
   end
@@ -172,13 +202,21 @@ defmodule NextLS do
     {:noreply, lsp}
   end
 
-  def handle_notification(%TextDocumentDidChange{}, lsp) do
+  def handle_notification(
+        %TextDocumentDidChange{
+          params: %{
+            text_document: %{uri: uri},
+            content_changes: [%{text: text}]
+          }
+        },
+        lsp
+      ) do
     for task <- Task.Supervisor.children(lsp.assigns.task_supervisor),
         task != lsp.assigns.runtime_task.pid do
       Process.exit(task, :kill)
     end
 
-    {:noreply, lsp}
+    {:noreply, put_in(lsp.assigns.documents[uri], String.split(text, "\n"))}
   end
 
   def handle_notification(
diff --git a/lib/next_ls/runtime.ex b/lib/next_ls/runtime.ex
index 9d7a00ce..03612c66 100644
--- a/lib/next_ls/runtime.ex
+++ b/lib/next_ls/runtime.ex
@@ -33,7 +33,7 @@ defmodule NextLS.Runtime do
   end
 
   def compile(server) do
-    GenServer.call(server, :compile)
+    GenServer.call(server, :compile, :infinity)
   end
 
   @impl GenServer
@@ -111,8 +111,6 @@ defmodule NextLS.Runtime do
   def handle_call(:compile, _, %{node: node} = state) do
     {_, errors} = :rpc.call(node, :_next_ls_private_compiler, :compile, [])
 
-    foo = "foo"
-
     Registry.dispatch(state.extension_registry, :extension, fn entries ->
       for {pid, _} <- entries do
         send(pid, {:compiler, errors})
diff --git a/test/next_ls_test.exs b/test/next_ls_test.exs
index 76b8cbf6..548bd149 100644
--- a/test/next_ls_test.exs
+++ b/test/next_ls_test.exs
@@ -181,4 +181,72 @@ defmodule NextLSTest do
     #   }
     # )
   end
+
+  test "formats", %{client: client} do
+    assert :ok ==
+             notify(client, %{
+               method: "initialized",
+               jsonrpc: "2.0",
+               params: %{}
+             })
+
+    notify client, %{
+      method: "textDocument/didOpen",
+      jsonrpc: "2.0",
+      params: %{
+        textDocument: %{
+          uri: "file://lib/foo/bar.ex",
+          languageId: "elixir",
+          version: 1,
+          text: """
+          defmodule Foo.Bar do
+            def run() do
+
+
+              :ok
+            end
+          end
+          """
+        }
+      }
+    }
+
+    request client, %{
+      method: "textDocument/formatting",
+      id: 2,
+      jsonrpc: "2.0",
+      params: %{
+        textDocument: %{
+          uri: "file://lib/foo/bar.ex"
+        },
+        options: %{
+          insertSpaces: true,
+          tabSize: 2
+        }
+      }
+    }
+
+    new_text =
+      """
+      defmodule Foo.Bar do
+        def run() do
+          :ok
+        end
+      end
+      """
+      |> String.trim()
+
+    assert_result(
+      2,
+      [
+        %{
+          "newText" => ^new_text,
+          "range" => %{
+            "start" => %{"character" => 0, "line" => 0},
+            "end" => %{"character" => 0, "line" => 8}
+          }
+        }
+      ]
+    )
+  end
 end
