Skip to content

Commit aabdda0

Browse files
authored
feat: basic lsp (#5)
1 parent d47b6d7 commit aabdda0

24 files changed

+872
-23
lines changed

Diff for: .dialyzer_ignore.exs

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[
2+
{"lib/next_ls/lsp_supervisor.ex", :exact_eq}
3+
]

Diff for: .formatter.exs

+18-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,20 @@
1-
# Used by "mix format"
21
[
3-
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
2+
locals_without_parens: [
3+
assert_result: 2,
4+
assert_notification: 2,
5+
assert_result: 3,
6+
assert_notification: 3,
7+
notify: 2,
8+
request: 2
9+
],
10+
line_length: 120,
11+
import_deps: [:gen_lsp],
12+
inputs: [
13+
"{mix,.formatter}.exs",
14+
"{config,lib,}/**/*.{ex,exs}",
15+
"test/next_ls_test.exs",
16+
"test/test_helper.exs",
17+
"test/next_ls/**/*.{ex,exs}",
18+
"priv/**/*.ex"
19+
]
420
]

Diff for: .github/workflows/ci.yaml

+11
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,17 @@ jobs:
3232
- name: Install Dependencies
3333
run: mix deps.get
3434

35+
- name: Start EPMD
36+
run: epmd -daemon
37+
38+
- name: Compile test project
39+
run: (cd test/support/project && mix deps.get && mix compile)
40+
41+
- name: Compile
42+
env:
43+
MIX_ENV: test
44+
run: mix compile
45+
3546
- name: Run Tests
3647
run: mix test
3748

Diff for: bin/nextls

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env -S elixir --sname undefined
2+
3+
System.no_halt(true)
4+
5+
Logger.configure(level: :none)
6+
7+
Mix.start()
8+
Mix.shell(Mix.Shell.Process)
9+
10+
default_version = "0.1.0" # x-release-please-version
11+
12+
Mix.install([{:next_ls, System.get_env("NEXTLS_VERSION", default_version)}])
13+
14+
Logger.configure(level: :info)
15+
16+
Application.ensure_all_started(:next_ls)

Diff for: bin/start

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env bash
2+
3+
# used for local development
4+
5+
cd "$(dirname "$0")"/.. || exit 1
6+
7+
elixir --sname undefined -S mix run --no-halt -e "Application.ensure_all_started(:next_ls)" -- "$@"

Diff for: lib/next_ls.ex

+228-11
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,235 @@
11
defmodule NextLS do
2-
@moduledoc """
3-
Documentation for `NextLS`.
4-
"""
2+
@moduledoc false
3+
use GenLSP
54

6-
@doc """
7-
Hello world.
5+
alias GenLSP.ErrorResponse
86

9-
## Examples
7+
alias GenLSP.Enumerations.{
8+
ErrorCodes,
9+
TextDocumentSyncKind
10+
}
1011

11-
iex> NextLS.hello()
12-
:world
12+
alias GenLSP.Notifications.{
13+
Exit,
14+
Initialized,
15+
TextDocumentDidChange,
16+
TextDocumentDidOpen,
17+
TextDocumentDidSave
18+
}
1319

14-
"""
15-
def hello do
16-
:world
20+
alias GenLSP.Requests.{Initialize, Shutdown}
21+
22+
alias GenLSP.Structures.{
23+
DidOpenTextDocumentParams,
24+
InitializeParams,
25+
InitializeResult,
26+
SaveOptions,
27+
ServerCapabilities,
28+
TextDocumentItem,
29+
TextDocumentSyncOptions
30+
}
31+
32+
def start_link(args) do
33+
{args, opts} = Keyword.split(args, [:task_supervisor, :runtime_supervisor])
34+
35+
GenLSP.start_link(__MODULE__, args, opts)
36+
end
37+
38+
@impl true
39+
def init(lsp, args) do
40+
task_supervisor = Keyword.fetch!(args, :task_supervisor)
41+
runtime_supervisor = Keyword.fetch!(args, :runtime_supervisor)
42+
43+
{:ok,
44+
assign(lsp,
45+
exit_code: 1,
46+
documents: %{},
47+
refresh_refs: %{},
48+
task_supervisor: task_supervisor,
49+
runtime_supervisor: runtime_supervisor,
50+
runtime_task: nil,
51+
ready: false
52+
)}
53+
end
54+
55+
@impl true
56+
def handle_request(
57+
%Initialize{params: %InitializeParams{root_uri: root_uri}},
58+
lsp
59+
) do
60+
{:reply,
61+
%InitializeResult{
62+
capabilities: %ServerCapabilities{
63+
text_document_sync: %TextDocumentSyncOptions{
64+
open_close: true,
65+
save: %SaveOptions{include_text: true},
66+
change: TextDocumentSyncKind.full()
67+
}
68+
},
69+
server_info: %{name: "NextLS"}
70+
}, assign(lsp, root_uri: root_uri)}
71+
end
72+
73+
def handle_request(%Shutdown{}, lsp) do
74+
{:reply, nil, assign(lsp, exit_code: 0)}
75+
end
76+
77+
def handle_request(%{method: method}, lsp) do
78+
GenLSP.warning(lsp, "[NextLS] Method Not Found: #{method}")
79+
80+
{:reply,
81+
%ErrorResponse{
82+
code: ErrorCodes.method_not_found(),
83+
message: "Method Not Found: #{method}"
84+
}, lsp}
85+
end
86+
87+
@impl true
88+
def handle_notification(%Initialized{}, lsp) do
89+
GenLSP.log(lsp, "[NextLS] LSP Initialized!")
90+
91+
working_dir = URI.parse(lsp.assigns.root_uri).path
92+
93+
GenLSP.log(lsp, "[NextLS] Booting runime...")
94+
95+
{:ok, runtime} =
96+
DynamicSupervisor.start_child(
97+
lsp.assigns.runtime_supervisor,
98+
{NextLS.Runtime, working_dir: working_dir, parent: self()}
99+
)
100+
101+
Process.monitor(runtime)
102+
103+
lsp = assign(lsp, runtime: runtime)
104+
105+
task =
106+
Task.Supervisor.async_nolink(lsp.assigns.task_supervisor, fn ->
107+
with false <-
108+
wait_until(fn ->
109+
NextLS.Runtime.ready?(runtime)
110+
end) do
111+
GenLSP.error(lsp, "Failed to start runtime")
112+
raise "Failed to boot runtime"
113+
end
114+
115+
GenLSP.log(lsp, "[NextLS] Runtime ready...")
116+
117+
:ready
118+
end)
119+
120+
{:noreply, assign(lsp, runtime_task: task)}
121+
end
122+
123+
def handle_notification(%TextDocumentDidSave{}, %{assigns: %{ready: false}} = lsp) do
124+
{:noreply, lsp}
125+
end
126+
127+
def handle_notification(
128+
%TextDocumentDidSave{
129+
params: %GenLSP.Structures.DidSaveTextDocumentParams{
130+
text: text,
131+
text_document: %{uri: uri}
132+
}
133+
},
134+
%{assigns: %{ready: true}} = lsp
135+
) do
136+
{:noreply, lsp |> then(&put_in(&1.assigns.documents[uri], String.split(text, "\n")))}
137+
end
138+
139+
def handle_notification(%TextDocumentDidChange{}, %{assigns: %{ready: false}} = lsp) do
140+
{:noreply, lsp}
141+
end
142+
143+
def handle_notification(%TextDocumentDidChange{}, lsp) do
144+
for task <- Task.Supervisor.children(lsp.assigns.task_supervisor),
145+
task != lsp.assigns.runtime_task do
146+
Process.exit(task, :kill)
147+
end
148+
149+
{:noreply, lsp}
150+
end
151+
152+
def handle_notification(
153+
%TextDocumentDidOpen{
154+
params: %DidOpenTextDocumentParams{
155+
text_document: %TextDocumentItem{text: text, uri: uri}
156+
}
157+
},
158+
lsp
159+
) do
160+
{:noreply, put_in(lsp.assigns.documents[uri], String.split(text, "\n"))}
161+
end
162+
163+
def handle_notification(%Exit{}, lsp) do
164+
System.halt(lsp.assigns.exit_code)
165+
166+
{:noreply, lsp}
167+
end
168+
169+
def handle_notification(_notification, lsp) do
170+
{:noreply, lsp}
171+
end
172+
173+
def handle_info({ref, resp}, %{assigns: %{refresh_refs: refs}} = lsp)
174+
when is_map_key(refs, ref) do
175+
Process.demonitor(ref, [:flush])
176+
{_token, refs} = Map.pop(refs, ref)
177+
178+
lsp =
179+
case resp do
180+
:ready ->
181+
assign(lsp, ready: true)
182+
183+
_ ->
184+
lsp
185+
end
186+
187+
{:noreply, assign(lsp, refresh_refs: refs)}
188+
end
189+
190+
def handle_info(
191+
{:DOWN, ref, :process, _pid, _reason},
192+
%{assigns: %{refresh_refs: refs}} = lsp
193+
)
194+
when is_map_key(refs, ref) do
195+
{_token, refs} = Map.pop(refs, ref)
196+
197+
{:noreply, assign(lsp, refresh_refs: refs)}
198+
end
199+
200+
def handle_info(
201+
{:DOWN, _ref, :process, runtime, _reason},
202+
%{assigns: %{runtime: runtime}} = lsp
203+
) do
204+
GenLSP.error(lsp, "[NextLS] The runtime has crashed")
205+
206+
{:noreply, assign(lsp, runtime: nil)}
207+
end
208+
209+
def handle_info({:log, message}, lsp) do
210+
GenLSP.log(lsp, String.trim(message))
211+
212+
{:noreply, lsp}
213+
end
214+
215+
def handle_info(_, lsp) do
216+
{:noreply, lsp}
217+
end
218+
219+
defp wait_until(cb) do
220+
wait_until(120, cb)
221+
end
222+
223+
defp wait_until(0, _cb) do
224+
false
225+
end
226+
227+
defp wait_until(n, cb) do
228+
if cb.() do
229+
true
230+
else
231+
Process.sleep(1000)
232+
wait_until(n - 1, cb)
233+
end
17234
end
18235
end

Diff for: lib/next_ls/application.ex

+1-4
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@ defmodule NextLS.Application do
77

88
@impl true
99
def start(_type, _args) do
10-
children = [
11-
# Starts a worker by calling: NextLS.Worker.start_link(arg)
12-
# {NextLS.Worker, arg}
13-
]
10+
children = [NextLS.LSPSupervisor]
1411

1512
# See https://hexdocs.pm/elixir/Supervisor.html
1613
# for other strategies and supported options

Diff for: lib/next_ls/lsp_supervisor.ex

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
defmodule NextLS.LSPSupervisor do
2+
@moduledoc false
3+
4+
use Supervisor
5+
6+
@env Mix.env()
7+
8+
def start_link(init_arg) do
9+
Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
10+
end
11+
12+
@impl true
13+
def init(_init_arg) do
14+
if @env == :test do
15+
:ignore
16+
else
17+
{opts, _} =
18+
OptionParser.parse!(System.argv(),
19+
strict: [stdio: :boolean, port: :integer]
20+
)
21+
22+
buffer_opts =
23+
cond do
24+
opts[:stdio] ->
25+
[]
26+
27+
is_integer(opts[:port]) ->
28+
IO.puts("Starting on port #{opts[:port]}")
29+
[communication: {GenLSP.Communication.TCP, [port: opts[:port]]}]
30+
31+
true ->
32+
raise "Unknown option"
33+
end
34+
35+
children = [
36+
{DynamicSupervisor, name: NextLS.RuntimeSupervisor},
37+
{Task.Supervisor, name: NextLS.TaskSupervisor},
38+
{GenLSP.Buffer, buffer_opts},
39+
{NextLS, task_supervisor: NextLS.TaskSupervisor, runtime_supervisor: NextLS.RuntimeSupervisor}
40+
]
41+
42+
Supervisor.init(children, strategy: :one_for_one)
43+
end
44+
end
45+
end

0 commit comments

Comments
 (0)