Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 109 additions & 9 deletions apps/forge/lib/forge/project.ex
Original file line number Diff line number Diff line change
Expand Up @@ -413,26 +413,52 @@ defmodule Forge.Project do
end
end

@doc """
Returns the `apps_path` configured in `mix.exs` when `project_path` is an
umbrella root, otherwise returns `nil`.
"""
def umbrella_apps_path(project_path) when is_binary(project_path) do
mix_exs_path = Path.join(project_path, "mix.exs")

with true <- File.exists?(mix_exs_path),
{:ok, source} <- File.read(mix_exs_path),
{:ok, ast} <- Code.string_to_quoted(source),
apps_path when is_binary(apps_path) <- extract_apps_path(ast) do
apps_path
else
_ -> nil
end
end

def find_parent_root_dir(path) do
path = Forge.Document.Path.from_uri(path)
path = path |> Path.expand() |> Path.dirname()
path = path |> Path.expand() |> path_or_parent_dir()
boundary = workspace_boundary_path()

segments = Path.split(path)

traverse_path(segments)
case traverse_path(segments, boundary) do
nil -> nil
root -> Document.Path.to_uri(root)
end
end

defp traverse_path([]), do: nil
defp traverse_path([], _boundary), do: nil

defp traverse_path(segments) do
defp traverse_path(segments, boundary) do
path = Path.join(segments)
mix_exs_path = Path.join(path, "mix.exs")

if File.exists?(mix_exs_path) do
Document.Path.to_uri(path)
else
{_, rest} = List.pop_at(segments, -1)
traverse_path(rest)
cond do
boundary_reached?(path, boundary) ->
nil

File.exists?(mix_exs_path) ->
umbrella_root_for(path, boundary) || path

true ->
{_, rest} = List.pop_at(segments, -1)
traverse_path(rest, boundary)
end
end

Expand All @@ -446,4 +472,78 @@ defmodule Forge.Project do
:ok
end
end

defp workspace_boundary_path do
case Forge.Workspace.get_workspace() do
%Forge.Workspace{root_path: root_path} when is_binary(root_path) ->
Path.expand(root_path)

_ ->
nil
end
end

defp boundary_reached?(_path, nil), do: false

defp boundary_reached?(path, boundary) do
expanded_path = Path.expand(path)

not Forge.Path.parent_path?(expanded_path, boundary)
end

defp path_or_parent_dir(path) do
if File.dir?(path) do
path
else
Path.dirname(path)
end
end

defp umbrella_root_for(project_path, boundary) do
project_path = Path.expand(project_path)
do_find_umbrella_root(Path.dirname(project_path), project_path, boundary)
end

defp do_find_umbrella_root(current_path, project_path, boundary) do
if !boundary_reached?(current_path, boundary) do
case umbrella_apps_path(current_path) do
apps_path when is_binary(apps_path) ->
apps_root = Path.expand(Path.join(current_path, apps_path))

if project_path == apps_root or Forge.Path.parent_path?(project_path, apps_root) do
current_path
else
next_parent(current_path, project_path, boundary)
end

_ ->
next_parent(current_path, project_path, boundary)
end
end
end

defp next_parent(current_path, project_path, boundary) do
parent = Path.dirname(current_path)

cond do
parent == current_path ->
nil

boundary_reached?(parent, boundary) ->
nil

true ->
do_find_umbrella_root(parent, project_path, boundary)
end
end

defp extract_apps_path(ast) do
{_ast, apps_path} =
Macro.prewalk(ast, nil, fn
{:apps_path, value} = node, nil when is_binary(value) -> {node, value}
node, acc -> {node, acc}
end)

apps_path
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
defmodule Search do
end
12 changes: 12 additions & 0 deletions apps/forge/test/fixtures/umbrella/packages/search/mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
defmodule Search.MixProject do
use Mix.Project

def project do
[
app: :search,
version: "0.1.0",
elixir: "~> 1.15",
deps: []
]
end
end
11 changes: 11 additions & 0 deletions apps/forge/test/fixtures/umbrella_custom_apps_path/mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
defmodule UmbrellaCustomAppsPath.MixProject do
use Mix.Project

def project do
[
apps_path: "packages",
version: "0.1.0",
deps: []
]
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
defmodule UmbrellaCustomAppsPath.First do
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
defmodule UmbrellaCustomAppsPath.First.MixProject do
use Mix.Project

def project do
[
app: :first,
version: "0.1.0",
elixir: "~> 1.15",
deps: []
]
end
end
149 changes: 149 additions & 0 deletions apps/forge/test/forge/project_umbrella_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
defmodule Forge.ProjectUmbrellaTest do
use ExUnit.Case, async: true

import Forge.Test.Fixtures

alias Forge.Document
alias Forge.Project
alias Forge.Workspace

defp umbrella_root, do: Path.join(fixtures_path(), "umbrella")
defp sub_app_path(name), do: Path.join([umbrella_root(), "apps", name])
defp package_project_path(name), do: Path.join([umbrella_root(), "packages", name])
defp custom_umbrella_root, do: Path.join(fixtures_path(), "umbrella_custom_apps_path")
defp custom_sub_app_path(name), do: Path.join([custom_umbrella_root(), "packages", name])

setup do
Workspace.set_workspace(nil)

on_exit(fn ->
Workspace.set_workspace(nil)
end)

:ok
end

describe "umbrella_apps_path/1" do
test "returns apps_path for an umbrella project" do
assert Project.umbrella_apps_path(umbrella_root()) == "apps"
end

test "returns nil for a sub-app directory" do
assert Project.umbrella_apps_path(sub_app_path("first")) == nil
end

test "returns nil for a directory without mix.exs" do
assert Project.umbrella_apps_path(Path.join(fixtures_path(), "nonexistent")) == nil
end

test "returns nil for a non-umbrella project" do
project_path = Path.join(fixtures_path(), "project")

if File.exists?(Path.join(project_path, "mix.exs")) do
assert Project.umbrella_apps_path(project_path) == nil
end
end

test "returns custom apps_path for an umbrella project" do
assert Project.umbrella_apps_path(custom_umbrella_root()) == "packages"
end
end

describe "find_parent_root_dir/1 with umbrella projects" do
test "returns umbrella root URI for a file inside a sub-app" do
file_uri = Document.Path.to_uri(Path.join(sub_app_path("first"), "lib/first.ex"))
result = Project.find_parent_root_dir(file_uri)

expected = Document.Path.to_uri(umbrella_root())
assert result == expected
end

test "returns umbrella root URI for a file in sub-app with same name as umbrella" do
file_uri = Document.Path.to_uri(Path.join(sub_app_path("umbrella"), "lib/umbrella.ex"))
result = Project.find_parent_root_dir(file_uri)

expected = Document.Path.to_uri(umbrella_root())
assert result == expected
end

test "returns umbrella root URI for sub-app mix.exs" do
file_uri = Document.Path.to_uri(Path.join(sub_app_path("second"), "mix.exs"))
result = Project.find_parent_root_dir(file_uri)

expected = Document.Path.to_uri(umbrella_root())
assert result == expected
end

test "returns non-umbrella package root URI for a file outside apps_path" do
file_uri = Document.Path.to_uri(Path.join(package_project_path("search"), "lib/search.ex"))
result = Project.find_parent_root_dir(file_uri)

expected = Document.Path.to_uri(package_project_path("search"))
assert result == expected
end

test "returns umbrella root URI when apps_path is set to a custom directory" do
file_uri = Document.Path.to_uri(Path.join(custom_sub_app_path("first"), "lib/first.ex"))
result = Project.find_parent_root_dir(file_uri)

expected = Document.Path.to_uri(custom_umbrella_root())
assert result == expected
end

test "returns normal project root for non-umbrella projects" do
project_path = Path.join(fixtures_path(), "project")

if File.exists?(Path.join(project_path, "mix.exs")) do
file_uri = Document.Path.to_uri(Path.join(project_path, "lib/project.ex"))
result = Project.find_parent_root_dir(file_uri)

expected = Document.Path.to_uri(project_path)
assert result == expected
end
end

test "does not traverse above workspace root while detecting umbrella root" do
Workspace.set_workspace(Workspace.new(sub_app_path("first")))

file_uri = Document.Path.to_uri(Path.join(sub_app_path("first"), "lib/first.ex"))
result = Project.find_parent_root_dir(file_uri)

expected = Document.Path.to_uri(sub_app_path("first"))
assert result == expected
end
end

describe "find_project/1 with umbrella projects" do
test "returns project rooted at umbrella root for sub-app files" do
file_uri = Document.Path.to_uri(Path.join(sub_app_path("first"), "lib/first.ex"))
project = Project.find_project(file_uri)

expected_root = Document.Path.to_uri(umbrella_root())
assert project.root_uri == expected_root
end

test "returns project rooted at umbrella root for sub-app with same name" do
file_uri = Document.Path.to_uri(Path.join(sub_app_path("umbrella"), "lib/umbrella.ex"))
project = Project.find_project(file_uri)

expected_root = Document.Path.to_uri(umbrella_root())
assert project.root_uri == expected_root
end

test "returns project rooted at a non-umbrella package outside apps_path" do
file_uri = Document.Path.to_uri(Path.join(package_project_path("search"), "lib/search.ex"))
project = Project.find_project(file_uri)

expected_root = Document.Path.to_uri(package_project_path("search"))
assert project.root_uri == expected_root
end

test "returns project rooted at umbrella root when apps_path is custom" do
file_uri = Document.Path.to_uri(Path.join(custom_sub_app_path("first"), "lib/first.ex"))
project = Project.find_project(file_uri)

expected_root = Document.Path.to_uri(custom_umbrella_root())
assert project.root_uri == expected_root
end
end
end
Loading