Skip to content

Commit

Permalink
Merge pull request #585 from JL102/find_root_dir_max_project_nesting
Browse files Browse the repository at this point in the history
Tweaked find_root_dir to accept a configurable max # of grandparents
  • Loading branch information
ckipp01 authored Aug 17, 2023
2 parents 92e2085 + b0e1fc3 commit fa2616f
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 18 deletions.
15 changes: 15 additions & 0 deletions doc/metals.txt
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,21 @@ initialize_or_attach({config})
{'build.sbt', 'build.sc', 'build.gradle',
'pom.xml', '.git'}
<

By default, when Metals detects a valid root_dir,
it will check 1 folder up to see if there is a
valid "parent" project above it. If you have a
more complex project with multiple nested sub-
projects, set the `find_root_dir_max_project_nesting`
key to an integer greater than 1.
Ex: Set `find_root_dir_max_project_nesting` to 2 if
this is your project tree:
build.sbt <- this is the desired root
a/
b/
- build.sbt <- subproject, not the desired root
- src/main/scala/Main.scala

If you need even more fine-grained control over
finding your root-dir (in cases where you have very
uncommon build layouts) you can also use the
Expand Down
6 changes: 5 additions & 1 deletion lua/metals/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,11 @@ local function validate_config(config, bufnr)

local find_root_dir = config.find_root_dir or root_dir.find_root_dir

config.root_dir = find_root_dir(config.root_patterns, bufname) or fn.expand("%:p:h")
-- Maximum parent folders to search AFTER the first project file (e.g. build.sbt) was found
local find_root_dir_max_project_nesting = config.find_root_dir_max_project_nesting or 1

config.root_dir = find_root_dir(config.root_patterns, bufname, find_root_dir_max_project_nesting)
or fn.expand("%:p:h")

local base_handlers = vim.tbl_extend("error", default_handlers, tvp.handlers)

Expand Down
44 changes: 31 additions & 13 deletions lua/metals/rootdir.lua
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,50 @@ local has_pattern = function(patterns, target)
end
end

--- NOTE: This only searches 2 levels deep to find nested build files.
--- NOTE: maxParentSearch lets you check up to a certain number of parent
--- folders to find nested build files.
--- Given a situation like the below one where you have a root build.sbt
--- and one in your module a, you want to ensure the root is correctly set as
--- the root one, not the a one. This checks the parent dir to ensure this.
--- build.sbt <-- this is the root
--- a/
--- - build.sbt <- this is not
--- - src/main/scala/Main.scala
local find_root_dir = function(patterns, startpath)
--- If your projects are multiple layers deep, set
--- config.find_root_dir_max_project_nesting to a greater number. Default is 1
--- for the behavior described above.
local find_root_dir = function(patterns, startpath, maxParentSearch)
local path = Path:new(startpath)
-- TODO if we don't find it do we really want to search / probably not... add a check for this
for _, parent in ipairs(path:parents()) do
-- First parent index in which we found a target file
local firstFoundIdx = nil
local ret = nil
local found = nil

for i, parent in ipairs(path:parents()) do
-- Exit loop before checking anything if we've exceeded the search limits
if firstFoundIdx and (i - firstFoundIdx > maxParentSearch) then
return ret
end

local pattern = has_pattern(patterns, parent)
if pattern then
local grandparent = Path:new(parent):parent()
-- If the pattern is found, we don't check for all patterns anymore,
-- instead only the one that was found. This will ensure that we don't
-- find a buid.sc is src/build.sc and also a .git in ./ causing it to
-- default to that instead of src for the root.
if has_pattern({ pattern }, grandparent) then
return grandparent.filename
else
return parent

-- We add an extra guard here that if there is a pattern and we've already found one
-- we make sure it's the same as the found one. For example we don't want to detect a
-- .scala-build nested and then look one deeper and see a .git and incorrectly mark .git
-- as the root.
if (pattern and not found) or (pattern and found == pattern) then
-- Mark the first parent that was found, so we can exit the loop when we've exhausted our search limits
if not firstFoundIdx then
found = pattern
firstFoundIdx = i
end
-- (over)write the return value with the highest parent found
ret = parent
end
end
-- In case we went through the entire loop (e.g. if maxParentSearch is really high)
return ret
end

return {
Expand Down
23 changes: 19 additions & 4 deletions tests/tests/root_dir_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ if not (multi_build_example:exists()) or not (mill_minimal:exists()) or not (sca
else
describe("The find root dir functionality", function()
it("should return nil when no pattern is detected", function()
local result = root_dir.find_root_dir({ "build.sbt" }, Path:new("."):expand()) or "was_nil"
local result = root_dir.find_root_dir({ "build.sbt" }, Path:new("."):expand(), 1) or "was_nil"
-- We expect nil here because nvim-metals has logic to then catch this nil and return the file that was opened.
-- No idea why but locally using nil here works fine by in Linux CI nil here keeps thinking I'm only using
-- one argument, so we instead replace it with "was_nil" which makes CI happy. who knows.
Expand All @@ -30,7 +30,20 @@ else
expected,
root_dir.find_root_dir(
{ "build.sbt" },
Path:new(multi_build_example:expand(), "core", "src", "main", "scala", "example", "Hello.scala").filename
Path:new(multi_build_example:expand(), "core", "src", "main", "scala", "example", "Hello.scala").filename,
1
)
)
end)

it("should correctly find the root in an odly nested multi-build sbt project", function()
local expected = multi_build_example:expand()
eq(
expected,
root_dir.find_root_dir(
{ "build.sbt" },
Path:new(multi_build_example:expand(), "other", "nested", "src", "main", "scala", "example", "Hello.scala").filename,
2 -- set to two here because we want to skip the other/nested/buid.sbt
)
)
end)
Expand All @@ -41,7 +54,8 @@ else
expected,
root_dir.find_root_dir(
{ "build.sc" },
Path:new(mill_minimal:expand(), "MillMinimal", "src", "example", "Hello.scala").filename
Path:new(mill_minimal:expand(), "MillMinimal", "src", "example", "Hello.scala").filename,
1
)
)
end)
Expand All @@ -52,7 +66,8 @@ else
local expected = Path:new(scala_cli:expand(), "src").filename
local result = root_dir.find_root_dir(
{ ".scala", ".scala-build", ".git" },
Path:new(scala_cli:expand(), "src", "Main.scala").filename
Path:new(scala_cli:expand(), "src", "Main.scala").filename,
1
)
eq(expected, result)
end)
Expand Down

0 comments on commit fa2616f

Please sign in to comment.