diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b20e935d..41b6cefb 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -21,7 +21,7 @@ jobs: shell: bash run: | mkdir -p /tmp/nvim - wget -q https://github.com/neovim/neovim/releases/download/nightly/nvim.appimage -O /tmp/nvim/nvim.appimage + wget -q https://github.com/neovim/neovim/releases/download/nightly/nvim-linux-x86_64.appimage -O /tmp/nvim/nvim.appimage cd /tmp/nvim chmod a+x ./nvim.appimage ./nvim.appimage --appimage-extract diff --git a/lua/flutter-tools/decorations.lua b/lua/flutter-tools/decorations.lua index c8ce3557..fa674f5c 100644 --- a/lua/flutter-tools/decorations.lua +++ b/lua/flutter-tools/decorations.lua @@ -13,9 +13,9 @@ local fn, api = vim.fn, vim.api ---Asynchronously read the data in the pubspec yaml and pass the results to a callback ---@param callback fun(data: string):nil local function read_pubspec(callback) - local root_patterns = { ".git", "pubspec.yaml" } + local conf = require("flutter-tools.config") local current_dir = fn.expand("%:p:h") - local root_dir = path.find_root(root_patterns, current_dir) or current_dir + local root_dir = path.find_root(conf.root_patterns, current_dir) or current_dir local pubspec_path = path.join(root_dir, "pubspec.yaml") local pubspec = Path:new(pubspec_path) pubspec:read(callback) diff --git a/lua/flutter-tools/utils/path.lua b/lua/flutter-tools/utils/path.lua index 49bc7f67..0242c0ae 100644 --- a/lua/flutter-tools/utils/path.lua +++ b/lua/flutter-tools/utils/path.lua @@ -108,7 +108,7 @@ function M.search_ancestors(startpath, func) end end -function M.find_root(patterns, startpath) +local function find_nearest_root(patterns, startpath) local function matcher(path) for _, pattern in ipairs(patterns) do if M.exists(vim.fn.glob(M.join(path, pattern))) then return path end @@ -117,6 +117,61 @@ function M.find_root(patterns, startpath) return M.search_ancestors(startpath, matcher) end +---@param pubspec_path string +---@return table|nil +local function parse_pubspec(pubspec_path) + if not M.is_file(pubspec_path) then return nil end + local content = vim.fn.readfile(pubspec_path) + if not content or #content == 0 then return nil end + local joined_content = table.concat(content, "\n") + local ok, parsed = pcall( + function() return require("flutter-tools.utils.yaml_parser").parse(joined_content) end + ) + if ok and parsed then return parsed end + return nil +end + +--- Checks for `resolution: workspace` in pubspec.yaml +---@param pubspec_path string +---@return boolean +local function is_pub_workspace_member(pubspec_path) + local pubspec = parse_pubspec(pubspec_path) + if not pubspec then return false end + return pubspec.resolution == "workspace" +end + +--- Checks for `workspace:` field in pubspec.yaml +---@param pubspec_path string +---@return boolean +local function is_pub_workspace_root(pubspec_path) + local pubspec = parse_pubspec(pubspec_path) + if not pubspec then return false end + return pubspec.workspace ~= nil +end + +--- Find project root, traversing up to workspace root if in a pub workspace +---@param patterns string[] +---@param startpath string +---@return string|nil +function M.find_root(patterns, startpath) + local root = find_nearest_root(patterns, startpath) + if not root then return nil end + + local pubspec_path = M.join(root, "pubspec.yaml") + if not is_pub_workspace_member(pubspec_path) then return root end + + -- Workspace member, traverse upward to find the workspace root + local parent = M.dirname(root) + if not parent or parent == root then return root end + + for dir in M.iterate_parents(parent) do + local workspace_pubspec = M.join(dir, "pubspec.yaml") + if is_pub_workspace_root(workspace_pubspec) then return dir end + end + + return root +end + function M.current_buffer_path() local current_buffer = api.nvim_get_current_buf() local current_buffer_path = api.nvim_buf_get_name(current_buffer) diff --git a/tests/path_spec.lua b/tests/path_spec.lua new file mode 100644 index 00000000..d636ea39 --- /dev/null +++ b/tests/path_spec.lua @@ -0,0 +1,83 @@ +local path = require("flutter-tools.utils.path") + +describe("path.find_root", function() + local test_dir + local workspace_root + local package_a + local package_b + local standalone + + before_each(function() + -- Use realpath to normalize (handles /var -> /private/var symlink on macOS) + local temp_base = vim.fn.tempname() + vim.fn.mkdir(temp_base, "p") + test_dir = vim.loop.fs_realpath(temp_base) + workspace_root = test_dir .. "/workspace" + package_a = workspace_root .. "/packages/package_a" + package_b = workspace_root .. "/packages/package_b" + standalone = test_dir .. "/standalone" + + vim.fn.mkdir(package_a, "p") + vim.fn.mkdir(package_b, "p") + vim.fn.mkdir(standalone, "p") + + vim.fn.writefile({ + "name: my_workspace", + "workspace:", + " - packages/package_a", + " - packages/package_b", + }, workspace_root .. "/pubspec.yaml") + + vim.fn.writefile({ + "name: package_a", + "resolution: workspace", + }, package_a .. "/pubspec.yaml") + + vim.fn.writefile({ + "name: package_b", + "resolution: workspace", + }, package_b .. "/pubspec.yaml") + + vim.fn.writefile({ + "name: standalone", + "version: 1.0.0", + }, standalone .. "/pubspec.yaml") + end) + + after_each(function() vim.fn.delete(test_dir, "rf") end) + + local patterns = { "pubspec.yaml" } + + it("should find workspace root from member package", function() + local file_path = package_a .. "/lib/main.dart" + vim.fn.mkdir(package_a .. "/lib", "p") + vim.fn.writefile({ "void main() {}" }, file_path) + + assert.are.equal(workspace_root, path.find_root(patterns, file_path)) + end) + + it("should find workspace root from nested directory", function() + local nested_dir = package_b .. "/lib/src/widgets" + vim.fn.mkdir(nested_dir, "p") + local file_path = nested_dir .. "/button.dart" + vim.fn.writefile({ "class Button {}" }, file_path) + + assert.are.equal(workspace_root, path.find_root(patterns, file_path)) + end) + + it("should return package root for non-workspace package", function() + local file_path = standalone .. "/lib/main.dart" + vim.fn.mkdir(standalone .. "/lib", "p") + vim.fn.writefile({ "void main() {}" }, file_path) + + assert.are.equal(standalone, path.find_root(patterns, file_path)) + end) + + it("should return workspace root when starting from workspace root", function() + local file_path = workspace_root .. "/tool/script.dart" + vim.fn.mkdir(workspace_root .. "/tool", "p") + vim.fn.writefile({ "void main() {}" }, file_path) + + assert.are.equal(workspace_root, path.find_root(patterns, file_path)) + end) +end)