Skip to content

Commit cf379a5

Browse files
committed
chore: use lsp workspace folder functionality
1 parent c0aa69b commit cf379a5

File tree

6 files changed

+102
-93
lines changed

6 files changed

+102
-93
lines changed

apps/expert/lib/expert.ex

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ defmodule Expert do
1414
GenLSP.Notifications.TextDocumentDidChange,
1515
GenLSP.Notifications.WorkspaceDidChangeConfiguration,
1616
GenLSP.Notifications.WorkspaceDidChangeWatchedFiles,
17+
GenLSP.Notifications.WorkspaceDidChangeWorkspaceFolders,
1718
GenLSP.Notifications.TextDocumentDidClose,
1819
GenLSP.Notifications.TextDocumentDidOpen,
1920
GenLSP.Notifications.TextDocumentDidSave,
2021
GenLSP.Notifications.Exit,
21-
GenLSP.Notifications.Initialized,
2222
GenLSP.Requests.Shutdown
2323
]
2424

@@ -47,13 +47,6 @@ defmodule Expert do
4747

4848
case State.initialize(state, request) do
4949
{:ok, response, state} ->
50-
# TODO: this should be gated behind the dynamic registration in the initialization params
51-
registrations = registrations()
52-
53-
if nil != GenLSP.request(lsp, registrations) do
54-
Logger.error("Failed to register capability")
55-
end
56-
5750
lsp = assign(lsp, state: state)
5851
{:ok, response} = Forge.Protocol.Convert.to_lsp(response)
5952

@@ -119,6 +112,17 @@ defmodule Expert do
119112
end
120113
end
121114

115+
def handle_notification(%GenLSP.Notifications.Initialized{}, lsp) do
116+
Logger.info("Server initialized, registering capabilities")
117+
registrations = registrations()
118+
119+
if nil != GenLSP.request(lsp, registrations) do
120+
Logger.error("Failed to register capability")
121+
end
122+
123+
{:noreply, lsp}
124+
end
125+
122126
def handle_notification(%mod{} = notification, lsp) when mod in @server_specific_messages do
123127
with {:ok, notification} <- Convert.to_native(notification),
124128
{:ok, state} <- apply_to_state(assigns(lsp).state, notification) do

apps/expert/lib/expert/configuration.ex

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,16 @@ defmodule Expert.Configuration do
2929

3030
@dialyzer {:nowarn_function, set_dialyzer_enabled: 2}
3131

32-
@spec new(Forge.uri(), map(), String.t() | nil) :: t
33-
def new(_root_uri, %Structures.ClientCapabilities{} = client_capabilities, client_name) do
32+
@spec new([Structures.WorkspaceFolder.t()], map(), String.t() | nil) :: t
33+
def new(workspace_folders, %Structures.ClientCapabilities{} = client_capabilities, client_name) do
3434
support = Support.new(client_capabilities)
3535

36-
%__MODULE__{support: support, projects: [], client_name: client_name}
36+
projects =
37+
for %{uri: uri} <- workspace_folders do
38+
Project.new(uri)
39+
end
40+
41+
%__MODULE__{support: support, projects: projects, client_name: client_name}
3742
|> tap(&set/1)
3843
end
3944

@@ -143,17 +148,29 @@ defmodule Expert.Configuration do
143148
{:ok, old_config}
144149
end
145150

146-
@spec add_project(t, Project.t()) :: t
147-
def add_project(%__MODULE__{} = config, %Project{} = project) do
148-
if Enum.any?(config.projects, &(&1.root_uri == project.root_uri)) do
149-
config
150-
else
151-
projects = [project | config.projects]
152-
new_config = %__MODULE__{config | projects: projects}
151+
@spec add_projects(t, [Project.t()]) :: t
152+
def add_projects(%__MODULE__{} = config, projects) do
153+
new_config =
154+
for project <- projects, reduce: config do
155+
config ->
156+
if Enum.any?(config.projects, &(&1.root_uri == project.root_uri)) do
157+
config
158+
else
159+
projects = [project | config.projects]
160+
%__MODULE__{config | projects: projects}
161+
end
162+
end
163+
164+
set(new_config)
153165

154-
set(new_config)
166+
new_config
167+
end
155168

156-
new_config
157-
end
169+
@spec remove_projects(t, [String.t()]) :: t
170+
def remove_projects(%__MODULE__{} = config, projects) do
171+
new_projects = Enum.reject(config.projects, &(&1.root_uri in projects))
172+
new_config = %__MODULE__{config | projects: new_projects}
173+
set(new_config)
174+
new_config
158175
end
159176
end

apps/expert/lib/expert/project/supervisor.ex

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,6 @@ defmodule Expert.Project.Supervisor do
77
alias Expert.Project.SearchListener
88
alias Forge.Project
99

10-
# TODO: this module is slightly weird
11-
# it is a module based supervisor, but has lots of dynamic supervisor functions
12-
# what I learned is that in Expert.Application, it is starting an ad hoc
13-
# dynamic supervisor, calling a function from this module
14-
# Later, when the server is initializing, it calls the start function in
15-
# this module, which starts a normal supervisor, which the start_link and
16-
# init callbacks will be called
17-
# my suggestion is to separate the dynamic supervisor functionalities from
18-
# this module into its own module
19-
2010
use Supervisor
2111

2212
def start_link(%Project{} = project) do

apps/expert/lib/expert/provider/handlers/go_to_definition.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule Expert.Provider.Handlers.GoToDefinition do
22
alias Expert.Configuration
3-
alias Forge.Project
43
alias Expert.EngineApi
4+
alias Forge.Project
55
alias GenLSP.Requests
66
alias GenLSP.Structures
77

apps/expert/lib/expert/state.ex

Lines changed: 59 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ defmodule Expert.State do
1818
defstruct configuration: nil,
1919
initialized?: false,
2020
shutdown_received?: false,
21-
in_flight_requests: %{}
21+
in_flight_requests: %{},
22+
workspace_folders: []
2223

2324
@supported_code_actions [
2425
Enumerations.CodeActionKind.quick_fix(),
@@ -55,11 +56,15 @@ defmodule Expert.State do
5556
|> Forge.Workspace.new()
5657
|> Forge.Workspace.set_workspace()
5758

58-
config = Configuration.new(event.root_uri, event.capabilities, client_name)
59+
config = Configuration.new(event.workspace_folders, event.capabilities, client_name)
5960
new_state = %__MODULE__{state | configuration: config, initialized?: true}
6061

6162
response = initialize_result()
6263

64+
for project <- config.projects do
65+
ensure_project_node_started(project)
66+
end
67+
6368
{:ok, response, new_state}
6469
end
6570

@@ -100,6 +105,39 @@ defmodule Expert.State do
100105
{:ok, state}
101106
end
102107

108+
def apply(%__MODULE__{} = state, %Notifications.WorkspaceDidChangeWorkspaceFolders{
109+
params:
110+
%Structures.DidChangeWorkspaceFoldersParams{
111+
event: %Structures.WorkspaceFoldersChangeEvent{added: added, removed: removed}
112+
} = params
113+
}) do
114+
workspace_folders = [added | state.workspace_folders] -- removed
115+
116+
removed_projects =
117+
for %{uri: uri} <- removed do
118+
project = Project.new(uri)
119+
Logger.info("Stopping project at uri #{uri}")
120+
Expert.Project.Supervisor.stop(project)
121+
project
122+
end
123+
124+
added_projects =
125+
for %{uri: uri} <- added do
126+
project = Project.new(uri)
127+
ensure_project_node_started(project)
128+
project
129+
end
130+
131+
config =
132+
state.configuration
133+
|> Configuration.add_projects(added_projects)
134+
|> Configuration.remove_projects(removed_projects)
135+
136+
state = %__MODULE__{state | configuration: config, workspace_folders: workspace_folders}
137+
138+
{:ok, state}
139+
end
140+
103141
def apply(%__MODULE__{} = state, %GenLSP.Notifications.TextDocumentDidChange{params: params}) do
104142
uri = params.text_document.uri
105143
version = params.text_document.version
@@ -136,18 +174,6 @@ defmodule Expert.State do
136174
language_id: language_id
137175
} = did_open.params.text_document
138176

139-
project = Project.find_project(uri)
140-
config = state.configuration
141-
142-
state =
143-
if is_nil(project) do
144-
state
145-
else
146-
maybe_start_project(project, config)
147-
config = Configuration.add_project(config, project)
148-
%__MODULE__{state | configuration: config}
149-
end
150-
151177
case Document.Store.open(uri, text, version, language_id) do
152178
:ok ->
153179
Logger.info("################### opened #{uri}")
@@ -216,19 +242,18 @@ defmodule Expert.State do
216242
{:ok, state}
217243
end
218244

219-
defp maybe_start_project(project, config) do
220-
already_started? =
221-
Enum.any?(config.projects, fn p ->
222-
p.root_uri == project.root_uri
223-
end)
224-
225-
if already_started? do
226-
:ok
227-
else
228-
Logger.info("Starting project at uri #{project.root_uri}")
229-
result = Expert.Project.Supervisor.start(project)
230-
Logger.info("result: #{inspect(result)}")
231-
:ok
245+
defp ensure_project_node_started(project) do
246+
case Expert.Project.Supervisor.start(project) do
247+
{:ok, _pid} ->
248+
Logger.info("Project node started for #{project.name}")
249+
250+
{:error, {reason, pid}} when reason in [:alread_started, :already_present] ->
251+
{:ok, pid}
252+
253+
{:error, reason} ->
254+
Logger.error(
255+
"Failed to start project node for #{project.name}: #{inspect(reason, pretty: true)}"
256+
)
232257
end
233258
end
234259

@@ -269,7 +294,13 @@ defmodule Expert.State do
269294
hover_provider: true,
270295
references_provider: true,
271296
text_document_sync: sync_options,
272-
workspace_symbol_provider: true
297+
workspace_symbol_provider: true,
298+
workspace: %{
299+
workspace_folders: %Structures.WorkspaceFoldersServerCapabilities{
300+
supported: true,
301+
change_notifications: true
302+
}
303+
}
273304
}
274305

275306
%GenLSP.Structures.InitializeResult{

apps/forge/lib/forge/project.ex

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -373,37 +373,4 @@ defmodule Forge.Project do
373373
Forge.Path.parent_path?(document.path, root_path(project))
374374
end)
375375
end
376-
377-
def find_project(path) do
378-
project_root = find_parent_root_dir(path)
379-
380-
if is_nil(project_root) do
381-
nil
382-
else
383-
new(project_root)
384-
end
385-
end
386-
387-
defp find_parent_root_dir(path) do
388-
path = Forge.Document.Path.from_uri(path)
389-
path = path |> Path.expand() |> Path.dirname()
390-
391-
segments = Path.split(path)
392-
393-
traverse_path(segments)
394-
end
395-
396-
defp traverse_path([]), do: nil
397-
398-
defp traverse_path(segments) do
399-
path = Path.join(segments)
400-
mix_exs_path = Path.join(path, "mix.exs")
401-
402-
if File.exists?(mix_exs_path) do
403-
Document.Path.to_uri(path)
404-
else
405-
{_, rest} = List.pop_at(segments, -1)
406-
traverse_path(rest)
407-
end
408-
end
409376
end

0 commit comments

Comments
 (0)