Skip to content

Commit 5c984bf

Browse files
committed
Support multiple root projects
This adds support for multiple projects within the server node, and starts a project node for each detected project within the workspace. It also changes the manager node to use the name of the workspace folder instead of the name of the first project that starts distribution.
1 parent d91e29a commit 5c984bf

File tree

23 files changed

+217
-50
lines changed

23 files changed

+217
-50
lines changed

apps/common/lib/lexical/path.ex

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
defmodule Lexical.Path do
2+
@moduledoc """
3+
Helpers for working with paths.
4+
"""
5+
6+
@doc """
7+
Checks if the `parent_path` is a parent directory of the `child_path`.
8+
9+
This function normalizes both paths and compares their segments to determine
10+
if the `parent_path` is a prefix of the `child_path`.
11+
12+
## Examples
13+
14+
iex> Lexical.Path.parent_path?("/home/user/docs/file.txt", "/home/user")
15+
true
16+
17+
iex> Lexical.Path.parent_path?("/home/user/docs/file.txt", "/home/admin")
18+
false
19+
20+
iex> Lexical.Path.parent_path?("/home/user/docs", "/home/user/docs")
21+
true
22+
23+
iex> Lexical.Path.parent_path?("/home/user/docs", "/home/user/docs/subdir")
24+
false
25+
"""
26+
def parent_path?(child_path, parent_path) do
27+
normalized_child = Path.expand(child_path)
28+
normalized_parent = Path.expand(parent_path)
29+
30+
child_segments = Path.split(normalized_child)
31+
parent_segments = Path.split(normalized_parent)
32+
33+
Enum.take(child_segments, length(parent_segments)) == parent_segments
34+
end
35+
end

apps/common/lib/lexical/project.ex

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,4 +321,24 @@ defmodule Lexical.Project do
321321
|> root_path()
322322
|> Path.basename()
323323
end
324+
325+
@doc """
326+
Finds the project that contains the given path.
327+
"""
328+
def project_for_uri(projects, uri) do
329+
path = Document.Path.from_uri(uri)
330+
331+
Enum.find(projects, fn project ->
332+
Lexical.Path.parent_path?(path, root_path(project))
333+
end)
334+
end
335+
336+
@doc """
337+
Finds the project that contains the given document.
338+
"""
339+
def project_for_document(projects, %Document{} = document) do
340+
Enum.find(projects, fn project ->
341+
Lexical.Path.parent_path?(document.path, root_path(project))
342+
end)
343+
end
324344
end
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
defmodule Lexical.Workspace do
2+
@moduledoc """
3+
The representation of the root directory where the server is running.
4+
"""
5+
6+
defstruct [:root_path]
7+
8+
@type t :: %__MODULE__{
9+
root_path: String.t() | nil
10+
}
11+
12+
def new(root_path) do
13+
%__MODULE__{root_path: root_path}
14+
end
15+
16+
def name(workspace) do
17+
Path.basename(workspace.root_path)
18+
end
19+
20+
def set_workspace(workspace) do
21+
:persistent_term.put({__MODULE__, :workspace}, workspace)
22+
end
23+
24+
def get_workspace do
25+
:persistent_term.get({__MODULE__, :workspace}, nil)
26+
end
27+
end

apps/remote_control/lib/lexical/remote_control.ex

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,15 @@ defmodule Lexical.RemoteControl do
119119
end
120120

121121
def manager_node_name(%Project{} = project) do
122-
:"manager-#{Project.name(project)}-#{Project.entropy(project)}@127.0.0.1"
122+
workspace = Lexical.Workspace.get_workspace()
123+
124+
workspace_name =
125+
case workspace do
126+
nil -> Project.name(project)
127+
_ -> Lexical.Workspace.name(workspace)
128+
end
129+
130+
:"manager-#{workspace_name}-#{Project.entropy(project)}@127.0.0.1"
123131
end
124132

125133
defp start_net_kernel(%Project{} = project) do

apps/remote_control/lib/lexical/remote_control/project_node_supervisor.ex

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,15 @@ defmodule Lexical.RemoteControl.ProjectNodeSupervisor do
1313
end
1414

1515
def start_link(%Project{} = project) do
16-
DynamicSupervisor.start_link(__MODULE__, project, name: __MODULE__, strategy: :one_for_one)
16+
DynamicSupervisor.start_link(__MODULE__, project, name: name(project), strategy: :one_for_one)
17+
end
18+
19+
defp name(%Project{} = project) do
20+
:"#{Project.name(project)}::project_node_supervisor"
1721
end
1822

1923
def start_project_node(%Project{} = project) do
20-
DynamicSupervisor.start_child(__MODULE__, ProjectNode.child_spec(project))
24+
DynamicSupervisor.start_child(name(project), ProjectNode.child_spec(project))
2125
end
2226

2327
@impl true

apps/server/lib/lexical/server/configuration.ex

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ defmodule Lexical.Server.Configuration do
1313
alias Lexical.Server.Configuration.Support
1414
alias Lexical.Server.Dialyzer
1515

16-
defstruct project: nil,
16+
defstruct projects: [],
1717
support: nil,
1818
client_name: nil,
1919
additional_watched_extensions: nil,
2020
dialyzer_enabled?: false
2121

2222
@type t :: %__MODULE__{
23-
project: Project.t() | nil,
23+
projects: [Project.t()],
2424
support: support | nil,
2525
client_name: String.t() | nil,
2626
additional_watched_extensions: [String.t()] | nil,
@@ -34,9 +34,9 @@ defmodule Lexical.Server.Configuration do
3434
@spec new(Lexical.uri(), map(), String.t() | nil) :: t
3535
def new(root_uri, %ClientCapabilities{} = client_capabilities, client_name) do
3636
support = Support.new(client_capabilities)
37-
project = Project.new(root_uri)
37+
projects = find_projects(root_uri)
3838

39-
%__MODULE__{support: support, project: project, client_name: client_name}
39+
%__MODULE__{support: support, projects: projects, client_name: client_name}
4040
|> tap(&set/1)
4141
end
4242

@@ -45,6 +45,34 @@ defmodule Lexical.Server.Configuration do
4545
struct!(__MODULE__, [support: Support.new()] ++ attrs)
4646
end
4747

48+
defp find_projects(root_uri) do
49+
root_path = Lexical.Document.Path.from_uri(root_uri)
50+
root_mix_exs = Path.join(root_path, "mix.exs")
51+
52+
projects =
53+
if File.exists?(root_mix_exs) do
54+
[Project.new(root_uri)]
55+
else
56+
find_multiroot_projects(root_path)
57+
end
58+
59+
if projects == [], do: [Project.new(root_uri)], else: projects
60+
end
61+
62+
defp find_multiroot_projects(root_path) do
63+
mix_exs_blob = Path.join([root_path, "**", "mix.exs"])
64+
65+
for mix_exs_path <- Path.wildcard(mix_exs_blob),
66+
"deps" not in Path.split(mix_exs_path) do
67+
project_uri =
68+
mix_exs_path
69+
|> Path.dirname()
70+
|> Lexical.Document.Path.to_uri()
71+
72+
Project.new(project_uri)
73+
end
74+
end
75+
4876
defp set(%__MODULE__{} = config) do
4977
:persistent_term.put(__MODULE__, config)
5078
end

apps/server/lib/lexical/server/provider/handlers/code_action.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
defmodule Lexical.Server.Provider.Handlers.CodeAction do
2+
alias Lexical.Project
23
alias Lexical.Protocol.Requests
34
alias Lexical.Protocol.Responses
45
alias Lexical.Protocol.Types
@@ -10,11 +11,13 @@ defmodule Lexical.Server.Provider.Handlers.CodeAction do
1011
require Logger
1112

1213
def handle(%Requests.CodeAction{} = request, %Configuration{} = config) do
14+
project = Project.project_for_document(config.projects, request.document)
15+
1316
diagnostics = Enum.map(request.context.diagnostics, &to_code_action_diagnostic/1)
1417

1518
code_actions =
1619
RemoteControl.Api.code_actions(
17-
config.project,
20+
project,
1821
request.document,
1922
request.range,
2023
diagnostics,

apps/server/lib/lexical/server/provider/handlers/code_lens.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ defmodule Lexical.Server.Provider.Handlers.CodeLens do
1414
require Logger
1515

1616
def handle(%Requests.CodeLens{} = request, %Configuration{} = config) do
17+
project = Project.project_for_document(config.projects, request.document)
18+
1719
lenses =
18-
case reindex_lens(config.project, request.document) do
20+
case reindex_lens(project, request.document) do
1921
nil -> []
2022
lens -> List.wrap(lens)
2123
end

apps/server/lib/lexical/server/provider/handlers/commands.ex

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ defmodule Lexical.Server.Provider.Handlers.Commands do
3030
response =
3131
case request.command do
3232
@reindex_name ->
33-
Logger.info("Reindex #{Project.name(config.project)}")
34-
reindex(config.project, request.id)
33+
project_names = Enum.map_join(config.projects, ", ", &Project.name/1)
34+
Logger.info("Reindex #{project_names}")
35+
reindex_all(config.projects, request.id)
3536

3637
invalid ->
3738
message = "#{invalid} is not a valid command"
@@ -41,16 +42,25 @@ defmodule Lexical.Server.Provider.Handlers.Commands do
4142
{:reply, response}
4243
end
4344

44-
defp reindex(%Project{} = project, request_id) do
45-
case RemoteControl.Api.reindex(project) do
46-
:ok ->
47-
Responses.ExecuteCommand.new(request_id, "ok")
45+
defp reindex_all(projects, request_id) do
46+
result =
47+
Enum.reduce_while(projects, :ok, fn project, _ ->
48+
case RemoteControl.Api.reindex(project) do
49+
:ok ->
50+
{:cont, :ok}
4851

49-
error ->
50-
Window.show_error_message("Indexing #{Project.name(project)} failed")
51-
Logger.error("Indexing command failed due to #{inspect(error)}")
52+
error ->
53+
Window.show_error_message("Indexing #{Project.name(project)} failed")
54+
Logger.error("Indexing command failed due to #{inspect(error)}")
5255

53-
internal_error(request_id, "Could not reindex: #{error}")
56+
{:halt, internal_error(request_id, "Could not reindex: #{error}")}
57+
end
58+
end)
59+
60+
if result == :ok do
61+
Responses.ExecuteCommand.new(request_id, "ok")
62+
else
63+
result
5464
end
5565
end
5666

apps/server/lib/lexical/server/provider/handlers/completion.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ defmodule Lexical.Server.Provider.Handlers.Completion do
22
alias Lexical.Ast
33
alias Lexical.Document
44
alias Lexical.Document.Position
5+
alias Lexical.Project
56
alias Lexical.Protocol.Requests
67
alias Lexical.Protocol.Responses
78
alias Lexical.Protocol.Types.Completion
@@ -11,9 +12,11 @@ defmodule Lexical.Server.Provider.Handlers.Completion do
1112
require Logger
1213

1314
def handle(%Requests.Completion{} = request, %Configuration{} = config) do
15+
project = Project.project_for_document(config.projects, request.document)
16+
1417
completions =
1518
CodeIntelligence.Completion.complete(
16-
config.project,
19+
project,
1720
document_analysis(request.document, request.position),
1821
request.position,
1922
request.context || Completion.Context.new(trigger_kind: :invoked)

0 commit comments

Comments
 (0)