Skip to content

Commit 2084725

Browse files
committed
Migrate to pull based configuration retrieval
Fixes #961 Fixes #921
1 parent 9a85168 commit 2084725

File tree

5 files changed

+158
-39
lines changed

5 files changed

+158
-39
lines changed

apps/language_server/lib/language_server.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ defmodule ElixirLS.LanguageServer do
2929

3030
@impl Application
3131
def stop(_state) do
32-
if not Application.get_env(:language_server, :restart, false) and ElixirLS.Utils.WireProtocol.io_intercepted?() do
32+
if not Application.get_env(:language_server, :restart, false) and
33+
ElixirLS.Utils.WireProtocol.io_intercepted?() do
3334
LanguageServer.JsonRpc.show_message(
3435
:error,
3536
"ElixirLS has crashed. See Output panel."

apps/language_server/lib/language_server/json_rpc.ex

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,18 +90,31 @@ defmodule ElixirLS.LanguageServer.JsonRpc do
9090
notify("window/logMessage", %{type: message_type_code(type), message: to_string(message)})
9191
end
9292

93-
def register_capability_request(server \\ __MODULE__, method, options) do
93+
def register_capability_request(server \\ __MODULE__, server_instance_id, method, options) do
94+
id = server_instance_id <> method <> JasonV.encode!(options)
95+
9496
send_request(server, "client/registerCapability", %{
9597
"registrations" => [
9698
%{
97-
"id" => :crypto.hash(:sha, method) |> Base.encode16(),
99+
"id" => :crypto.hash(:sha, id) |> Base.encode16(),
98100
"method" => method,
99101
"registerOptions" => options
100102
}
101103
]
102104
})
103105
end
104106

107+
def get_configuration_request(server \\ __MODULE__, scope_uri, section) do
108+
send_request(server, "workspace/configuration", %{
109+
"items" => [
110+
%{
111+
"scopeUri" => scope_uri,
112+
"section" => section
113+
}
114+
]
115+
})
116+
end
117+
105118
def show_message_request(server \\ __MODULE__, type, message, actions) do
106119
send_request(server, "window/showMessageRequest", %{
107120
"type" => message_type_code(type),

apps/language_server/lib/language_server/server.ex

Lines changed: 102 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ defmodule ElixirLS.LanguageServer.Server do
6161
# Tracks source files that are currently open in the editor
6262
source_files: %{},
6363
awaiting_contracts: [],
64-
supports_dynamic: false,
6564
mix_project?: false,
6665
mix_env: nil,
6766
mix_target: nil,
@@ -91,6 +90,8 @@ defmodule ElixirLS.LanguageServer.Server do
9190
".sface"
9291
]
9392

93+
@default_config_timeout 3
94+
9495
## Client API
9596

9697
def start_link(name \\ nil) do
@@ -235,21 +236,18 @@ defmodule ElixirLS.LanguageServer.Server do
235236
end
236237

237238
@impl GenServer
238-
def handle_info(:default_config, state = %__MODULE__{}) do
239-
state =
240-
case state do
241-
%{settings: nil} ->
242-
Logger.warning(
243-
"Did not receive workspace/didChangeConfiguration notification after 5 seconds. " <>
244-
"Using default settings."
245-
)
246-
247-
set_settings(state, %{})
239+
def handle_info(:default_config, state = %__MODULE__{settings: nil}) do
240+
Logger.warning(
241+
"Did not receive workspace/didChangeConfiguration notification after #{@default_config_timeout} seconds. " <>
242+
"The server will use default config."
243+
)
248244

249-
_ ->
250-
state
251-
end
245+
state = set_settings(state, %{})
246+
{:noreply, state}
247+
end
252248

249+
def handle_info(:default_config, state = %__MODULE__{}) do
250+
# we got workspace/didChangeConfiguration in time, nothing to do here
253251
{:noreply, state}
254252
end
255253

@@ -293,11 +291,67 @@ defmodule ElixirLS.LanguageServer.Server do
293291
## Helpers
294292

295293
defp handle_notification(notification("initialized"), state = %__MODULE__{}) do
296-
# If we don't receive workspace/didChangeConfiguration for 5 seconds, use default settings
297-
Process.send_after(self(), :default_config, 5000)
294+
# according to https://github.com/microsoft/language-server-protocol/issues/567#issuecomment-1060605611
295+
# this is the best point to pull configuration
298296

299-
if state.supports_dynamic do
300-
add_watched_extensions(state, @default_watched_extensions)
297+
supports_get_configuration =
298+
get_in(state.client_capabilities, [
299+
"workspace",
300+
"configuration"
301+
])
302+
303+
state =
304+
if supports_get_configuration do
305+
response = JsonRpc.get_configuration_request(state.root_uri, "elixirLS")
306+
307+
case response do
308+
{:ok, [result]} when is_map(result) ->
309+
Logger.info(
310+
"Received client configuration via workspace/configuration\n#{inspect(result)}"
311+
)
312+
313+
set_settings(state, result)
314+
315+
other ->
316+
Logger.error("Cannot get client configuration: #{inspect(other)}")
317+
state
318+
end
319+
else
320+
Logger.info("Client does not support workspace/configuration request")
321+
state
322+
end
323+
324+
unless state.settings do
325+
# We still don't have the configuration. Let's wait for workspace/didChangeConfiguration
326+
Process.send_after(self(), :default_config, @default_config_timeout * 1000)
327+
end
328+
329+
supports_dynamic_configuration_change_registration =
330+
get_in(state.client_capabilities, [
331+
"workspace",
332+
"didChangeConfiguration",
333+
"dynamicRegistration"
334+
])
335+
336+
if supports_dynamic_configuration_change_registration do
337+
Logger.info("Registering for workspace/didChangeConfiguration notifications")
338+
listen_for_configuration_changes(state.server_instance_id)
339+
else
340+
Logger.info("Client does not support workspace/didChangeConfiguration dynamic registration")
341+
end
342+
343+
supports_dynamic_file_watcher_registration =
344+
get_in(state.client_capabilities, [
345+
"workspace",
346+
"didChangeWatchedFiles",
347+
"dynamicRegistration"
348+
])
349+
350+
if supports_dynamic_file_watcher_registration do
351+
Logger.info("Registering for workspace/didChangeWatchedFiles notifications")
352+
add_watched_extensions(state.server_instance_id, @default_watched_extensions)
353+
else
354+
Logger.info("Client does not support workspace/didChangeWatchedFiles dynamic registration")
301355
end
302356

303357
state
@@ -321,6 +375,10 @@ defmodule ElixirLS.LanguageServer.Server do
321375
# the `projectDir` or `mixEnv` settings. If the settings don't match the format expected, leave
322376
# settings unchanged or set default settings if this is the first request.
323377
defp handle_notification(did_change_configuration(changed_settings), state = %__MODULE__{}) do
378+
Logger.info(
379+
"Received client configuration via workspace/didChangeConfiguration:\n#{inspect(changed_settings)}"
380+
)
381+
324382
prev_settings = state.settings || %{}
325383

326384
new_settings =
@@ -569,19 +627,10 @@ defmodule ElixirLS.LanguageServer.Server do
569627
state
570628
end
571629

572-
# Explicitly request file watchers from the client if supported
573-
supports_dynamic =
574-
get_in(client_capabilities, [
575-
"textDocument",
576-
"codeAction",
577-
"dynamicRegistration"
578-
])
579-
580630
state = %{
581631
state
582632
| client_capabilities: client_capabilities,
583-
server_instance_id: server_instance_id,
584-
supports_dynamic: supports_dynamic
633+
server_instance_id: server_instance_id
585634
}
586635

587636
{:ok,
@@ -1161,7 +1210,8 @@ defmodule ElixirLS.LanguageServer.Server do
11611210
|> set_mix_target(mix_target)
11621211
|> set_project_dir(project_dir)
11631212
|> set_dialyzer_enabled(enable_dialyzer)
1164-
|> add_watched_extensions(additional_watched_extensions)
1213+
1214+
add_watched_extensions(state.server_instance_id, additional_watched_extensions)
11651215

11661216
maybe_rebuild(state)
11671217
state = create_gitignore(state)
@@ -1173,25 +1223,42 @@ defmodule ElixirLS.LanguageServer.Server do
11731223
trigger_build(%{state | settings: settings})
11741224
end
11751225

1176-
defp add_watched_extensions(state = %__MODULE__{}, []) do
1177-
state
1226+
defp add_watched_extensions(_server_instance_id, []) do
1227+
:ok
11781228
end
11791229

1180-
defp add_watched_extensions(state = %__MODULE__{}, exts) when is_list(exts) do
1230+
defp add_watched_extensions(server_instance_id, exts) when is_list(exts) do
11811231
case JsonRpc.register_capability_request(
1232+
server_instance_id,
11821233
"workspace/didChangeWatchedFiles",
11831234
%{
11841235
"watchers" => Enum.map(exts, &%{"globPattern" => "**/*" <> &1})
11851236
}
11861237
) do
11871238
{:ok, nil} ->
1188-
:ok
1239+
Logger.info("client/registerCapability succeeded")
11891240

11901241
other ->
11911242
Logger.error("client/registerCapability returned: #{inspect(other)}")
11921243
end
1244+
end
11931245

1194-
state
1246+
defp listen_for_configuration_changes(server_instance_id) do
1247+
# the schema is not documented in official LSP docs
1248+
# using https://github.com/microsoft/vscode-languageserver-node/blob/7792b0b21c994cc9bebc3117eeb652a22e2d9e1f/protocol/src/common/protocol.ts#L1504C18-L1504C59
1249+
case JsonRpc.register_capability_request(
1250+
server_instance_id,
1251+
"workspace/didChangeConfiguration",
1252+
%{
1253+
"section" => "elixirLS"
1254+
}
1255+
) do
1256+
{:ok, nil} ->
1257+
Logger.info("client/registerCapability succeeded")
1258+
1259+
other ->
1260+
Logger.error("client/registerCapability returned: #{inspect(other)}")
1261+
end
11951262
end
11961263

11971264
defp set_dialyzer_enabled(state = %__MODULE__{}, enable_dialyzer) do
@@ -1221,6 +1288,7 @@ defmodule ElixirLS.LanguageServer.Server do
12211288
:warning,
12221289
"Environment variables change detected. ElixirLS will restart"
12231290
)
1291+
12241292
# sleep so the client has time to show the message
12251293
Process.sleep(5000)
12261294
ElixirLS.LanguageServer.restart()

apps/language_server/test/server_test.exs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
defmodule ElixirLS.LanguageServer.ServerTest do
2-
alias ElixirLS.LanguageServer.{Server, Protocol, SourceFile, Tracer, Build}
2+
alias ElixirLS.LanguageServer.{Server, Protocol, SourceFile, Tracer, Build, JsonRpc}
33
alias ElixirLS.Utils.PacketCapture
44
alias ElixirLS.LanguageServer.Test.FixtureHelpers
55
alias ElixirLS.LanguageServer.Test.ServerTestHelpers
@@ -59,6 +59,43 @@ defmodule ElixirLS.LanguageServer.ServerTest do
5959
assert_receive(%{"id" => 1, "result" => %{"capabilities" => %{}}}, 1000)
6060
end
6161

62+
test "gets configuration after initialized notification if client supports it", %{
63+
server: server
64+
} do
65+
Server.receive_packet(
66+
server,
67+
initialize_req(1, root_uri(), %{
68+
"workspace" => %{
69+
"configuration" => true
70+
}
71+
})
72+
)
73+
74+
assert_receive(%{"id" => 1, "result" => %{"capabilities" => %{}}}, 1000)
75+
Server.receive_packet(server, notification("initialized"))
76+
uri = root_uri()
77+
78+
assert_receive(
79+
%{
80+
"id" => 1,
81+
"method" => "workspace/configuration",
82+
"params" => %{"items" => [%{"scopeUri" => ^uri, "section" => "elixirLS"}]}
83+
},
84+
1000
85+
)
86+
87+
JsonRpc.receive_packet(
88+
response(1, [
89+
%{
90+
"mixEnv" => "dev",
91+
"autoBuild" => false
92+
}
93+
])
94+
)
95+
96+
assert :sys.get_state(server).mix_env == "dev"
97+
end
98+
6299
test "Execute commands should include the server instance id", %{server: server} do
63100
# If a command does not include the server instance id then it will cause
64101
# vscode-elixir-ls to fail to start up on multi-root workspaces.

guides/initialization.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ Upon receiving the [initialize request](https://microsoft.github.io/language-ser
8181

8282
The delayed message is important because some clients might send a message `workspace/didChangeConfiguration`. If that happens, it will start the build with a different configuration (for example, a different MIX_ENV).
8383

84-
If a notification `workspace/didChangeConfiguration` or the delayed message is handled, it updates the server settings and triggers builders/analyzers and so on.
84+
If a notification `workspace/didChangeConfiguration` is not received in 3s the server will try to get configuration via `workspace/configuration` request. If that request is not supported a default configuration is used. Finally, the server triggers builders/analyzers.
8585

8686
## Starts building/analyzing the project
8787

0 commit comments

Comments
 (0)