diff --git a/doc/metals.txt b/doc/metals.txt index 6afce65..8b8145f 100644 --- a/doc/metals.txt +++ b/doc/metals.txt @@ -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 diff --git a/lua/metals/config.lua b/lua/metals/config.lua index 16b6a13..68c8ec4 100644 --- a/lua/metals/config.lua +++ b/lua/metals/config.lua @@ -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) diff --git a/lua/metals/rootdir.lua b/lua/metals/rootdir.lua index 83b8c2e..71fedce 100644 --- a/lua/metals/rootdir.lua +++ b/lua/metals/rootdir.lua @@ -11,7 +11,8 @@ 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. @@ -19,24 +20,41 @@ end --- 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 { diff --git a/tests/tests/root_dir_spec.lua b/tests/tests/root_dir_spec.lua index 5bc794e..a7ac578 100644 --- a/tests/tests/root_dir_spec.lua +++ b/tests/tests/root_dir_spec.lua @@ -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. @@ -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) @@ -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) @@ -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)