Skip to content

Commit

Permalink
feat: basic lsp (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
mhanberg committed Jun 14, 2023
1 parent d47b6d7 commit aabdda0
Show file tree
Hide file tree
Showing 24 changed files with 872 additions and 23 deletions.
3 changes: 3 additions & 0 deletions .dialyzer_ignore.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[
{"lib/next_ls/lsp_supervisor.ex", :exact_eq}
]
20 changes: 18 additions & 2 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
locals_without_parens: [
assert_result: 2,
assert_notification: 2,
assert_result: 3,
assert_notification: 3,
notify: 2,
request: 2
],
line_length: 120,
import_deps: [:gen_lsp],
inputs: [
"{mix,.formatter}.exs",
"{config,lib,}/**/*.{ex,exs}",
"test/next_ls_test.exs",
"test/test_helper.exs",
"test/next_ls/**/*.{ex,exs}",
"priv/**/*.ex"
]
]
11 changes: 11 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ jobs:
- name: Install Dependencies
run: mix deps.get

- name: Start EPMD
run: epmd -daemon

- name: Compile test project
run: (cd test/support/project && mix deps.get && mix compile)

- name: Compile
env:
MIX_ENV: test
run: mix compile

- name: Run Tests
run: mix test

Expand Down
16 changes: 16 additions & 0 deletions bin/nextls
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env -S elixir --sname undefined

System.no_halt(true)

Logger.configure(level: :none)

Mix.start()
Mix.shell(Mix.Shell.Process)

default_version = "0.1.0" # x-release-please-version

Mix.install([{:next_ls, System.get_env("NEXTLS_VERSION", default_version)}])

Logger.configure(level: :info)

Application.ensure_all_started(:next_ls)
7 changes: 7 additions & 0 deletions bin/start
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env bash

# used for local development

cd "$(dirname "$0")"/.. || exit 1

elixir --sname undefined -S mix run --no-halt -e "Application.ensure_all_started(:next_ls)" -- "$@"
239 changes: 228 additions & 11 deletions lib/next_ls.ex
Original file line number Diff line number Diff line change
@@ -1,18 +1,235 @@
defmodule NextLS do
@moduledoc """
Documentation for `NextLS`.
"""
@moduledoc false
use GenLSP

@doc """
Hello world.
alias GenLSP.ErrorResponse

## Examples
alias GenLSP.Enumerations.{
ErrorCodes,
TextDocumentSyncKind
}

iex> NextLS.hello()
:world
alias GenLSP.Notifications.{
Exit,
Initialized,
TextDocumentDidChange,
TextDocumentDidOpen,
TextDocumentDidSave
}

"""
def hello do
:world
alias GenLSP.Requests.{Initialize, Shutdown}

alias GenLSP.Structures.{
DidOpenTextDocumentParams,
InitializeParams,
InitializeResult,
SaveOptions,
ServerCapabilities,
TextDocumentItem,
TextDocumentSyncOptions
}

def start_link(args) do
{args, opts} = Keyword.split(args, [:task_supervisor, :runtime_supervisor])

GenLSP.start_link(__MODULE__, args, opts)
end

@impl true
def init(lsp, args) do
task_supervisor = Keyword.fetch!(args, :task_supervisor)
runtime_supervisor = Keyword.fetch!(args, :runtime_supervisor)

{:ok,
assign(lsp,
exit_code: 1,
documents: %{},
refresh_refs: %{},
task_supervisor: task_supervisor,
runtime_supervisor: runtime_supervisor,
runtime_task: nil,
ready: false
)}
end

@impl true
def handle_request(
%Initialize{params: %InitializeParams{root_uri: root_uri}},
lsp
) do
{:reply,
%InitializeResult{
capabilities: %ServerCapabilities{
text_document_sync: %TextDocumentSyncOptions{
open_close: true,
save: %SaveOptions{include_text: true},
change: TextDocumentSyncKind.full()
}
},
server_info: %{name: "NextLS"}
}, assign(lsp, root_uri: root_uri)}
end

def handle_request(%Shutdown{}, lsp) do
{:reply, nil, assign(lsp, exit_code: 0)}
end

def handle_request(%{method: method}, lsp) do
GenLSP.warning(lsp, "[NextLS] Method Not Found: #{method}")

{:reply,
%ErrorResponse{
code: ErrorCodes.method_not_found(),
message: "Method Not Found: #{method}"
}, lsp}
end

@impl true
def handle_notification(%Initialized{}, lsp) do
GenLSP.log(lsp, "[NextLS] LSP Initialized!")

working_dir = URI.parse(lsp.assigns.root_uri).path

GenLSP.log(lsp, "[NextLS] Booting runime...")

{:ok, runtime} =
DynamicSupervisor.start_child(
lsp.assigns.runtime_supervisor,
{NextLS.Runtime, working_dir: working_dir, parent: self()}
)

Process.monitor(runtime)

lsp = assign(lsp, runtime: runtime)

task =
Task.Supervisor.async_nolink(lsp.assigns.task_supervisor, fn ->
with false <-
wait_until(fn ->
NextLS.Runtime.ready?(runtime)
end) do
GenLSP.error(lsp, "Failed to start runtime")
raise "Failed to boot runtime"
end

GenLSP.log(lsp, "[NextLS] Runtime ready...")

:ready
end)

{:noreply, assign(lsp, runtime_task: task)}
end

def handle_notification(%TextDocumentDidSave{}, %{assigns: %{ready: false}} = lsp) do
{:noreply, lsp}
end

def handle_notification(
%TextDocumentDidSave{
params: %GenLSP.Structures.DidSaveTextDocumentParams{
text: text,
text_document: %{uri: uri}
}
},
%{assigns: %{ready: true}} = lsp
) do
{:noreply, lsp |> then(&put_in(&1.assigns.documents[uri], String.split(text, "\n")))}
end

def handle_notification(%TextDocumentDidChange{}, %{assigns: %{ready: false}} = lsp) do
{:noreply, lsp}
end

def handle_notification(%TextDocumentDidChange{}, lsp) do
for task <- Task.Supervisor.children(lsp.assigns.task_supervisor),
task != lsp.assigns.runtime_task do
Process.exit(task, :kill)
end

{:noreply, lsp}
end

def handle_notification(
%TextDocumentDidOpen{
params: %DidOpenTextDocumentParams{
text_document: %TextDocumentItem{text: text, uri: uri}
}
},
lsp
) do
{:noreply, put_in(lsp.assigns.documents[uri], String.split(text, "\n"))}
end

def handle_notification(%Exit{}, lsp) do
System.halt(lsp.assigns.exit_code)

{:noreply, lsp}
end

def handle_notification(_notification, lsp) do
{:noreply, lsp}
end

def handle_info({ref, resp}, %{assigns: %{refresh_refs: refs}} = lsp)
when is_map_key(refs, ref) do
Process.demonitor(ref, [:flush])
{_token, refs} = Map.pop(refs, ref)

lsp =
case resp do
:ready ->
assign(lsp, ready: true)

_ ->
lsp
end

{:noreply, assign(lsp, refresh_refs: refs)}
end

def handle_info(
{:DOWN, ref, :process, _pid, _reason},
%{assigns: %{refresh_refs: refs}} = lsp
)
when is_map_key(refs, ref) do
{_token, refs} = Map.pop(refs, ref)

{:noreply, assign(lsp, refresh_refs: refs)}
end

def handle_info(
{:DOWN, _ref, :process, runtime, _reason},
%{assigns: %{runtime: runtime}} = lsp
) do
GenLSP.error(lsp, "[NextLS] The runtime has crashed")

{:noreply, assign(lsp, runtime: nil)}
end

def handle_info({:log, message}, lsp) do
GenLSP.log(lsp, String.trim(message))

{:noreply, lsp}
end

def handle_info(_, lsp) do
{:noreply, lsp}
end

defp wait_until(cb) do
wait_until(120, cb)
end

defp wait_until(0, _cb) do
false
end

defp wait_until(n, cb) do
if cb.() do
true
else
Process.sleep(1000)
wait_until(n - 1, cb)
end
end
end
5 changes: 1 addition & 4 deletions lib/next_ls/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ defmodule NextLS.Application do

@impl true
def start(_type, _args) do
children = [
# Starts a worker by calling: NextLS.Worker.start_link(arg)
# {NextLS.Worker, arg}
]
children = [NextLS.LSPSupervisor]

# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
Expand Down
45 changes: 45 additions & 0 deletions lib/next_ls/lsp_supervisor.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
defmodule NextLS.LSPSupervisor do
@moduledoc false

use Supervisor

@env Mix.env()

def start_link(init_arg) do
Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end

@impl true
def init(_init_arg) do
if @env == :test do
:ignore
else
{opts, _} =
OptionParser.parse!(System.argv(),
strict: [stdio: :boolean, port: :integer]
)

buffer_opts =
cond do
opts[:stdio] ->
[]

is_integer(opts[:port]) ->
IO.puts("Starting on port #{opts[:port]}")
[communication: {GenLSP.Communication.TCP, [port: opts[:port]]}]

true ->
raise "Unknown option"
end

children = [
{DynamicSupervisor, name: NextLS.RuntimeSupervisor},
{Task.Supervisor, name: NextLS.TaskSupervisor},
{GenLSP.Buffer, buffer_opts},
{NextLS, task_supervisor: NextLS.TaskSupervisor, runtime_supervisor: NextLS.RuntimeSupervisor}
]

Supervisor.init(children, strategy: :one_for_one)
end
end
end
Loading

0 comments on commit aabdda0

Please sign in to comment.