From c183be1f85b6b274bcba81d56b38bffeac7ec1b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tautvydas=20S=CC=8Cidlauskas?= Date: Sat, 3 Jan 2026 22:46:06 +0200 Subject: [PATCH 1/3] feat: add pub workspace support for LSP root detection Make find_root workspace-aware so that when opening a file in a pub workspace member package (pubspec.yaml has 'resolution: workspace'), the LSP server is rooted at the workspace root instead of the member package. This ensures a single LSP instance for the entire workspace. Fixes #504 --- lua/flutter-tools/decorations.lua | 4 +- lua/flutter-tools/utils/path.lua | 61 +++++++++++++++++++++- tests/path_spec.lua | 85 +++++++++++++++++++++++++++++++ 3 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 tests/path_spec.lua 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..98e4e383 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,65 @@ 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..ea9b8f69 --- /dev/null +++ b/tests/path_spec.lua @@ -0,0 +1,85 @@ +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) From c41a4b62a088deb94c5e90e086538dc7879457a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tautvydas=20S=CC=8Cidlauskas?= Date: Sat, 3 Jan 2026 22:58:48 +0200 Subject: [PATCH 2/3] fix(ci): update nightly appimage filename --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From a0e6baf34ba367703fb9f56a0223427c93876eb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tautvydas=20S=CC=8Cidlauskas?= Date: Sat, 3 Jan 2026 23:01:10 +0200 Subject: [PATCH 3/3] refactor(path): condense if statements to single-line form for readability --- lua/flutter-tools/utils/path.lua | 14 +++++--------- tests/path_spec.lua | 4 +--- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/lua/flutter-tools/utils/path.lua b/lua/flutter-tools/utils/path.lua index 98e4e383..0242c0ae 100644 --- a/lua/flutter-tools/utils/path.lua +++ b/lua/flutter-tools/utils/path.lua @@ -124,9 +124,9 @@ local function parse_pubspec(pubspec_path) 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) + 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 @@ -158,9 +158,7 @@ function M.find_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 + 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) @@ -168,9 +166,7 @@ function M.find_root(patterns, startpath) 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 + if is_pub_workspace_root(workspace_pubspec) then return dir end end return root diff --git a/tests/path_spec.lua b/tests/path_spec.lua index ea9b8f69..d636ea39 100644 --- a/tests/path_spec.lua +++ b/tests/path_spec.lua @@ -44,9 +44,7 @@ describe("path.find_root", function() }, standalone .. "/pubspec.yaml") end) - after_each(function() - vim.fn.delete(test_dir, "rf") - end) + after_each(function() vim.fn.delete(test_dir, "rf") end) local patterns = { "pubspec.yaml" }