From 08df51cf7aa3167d56fa4e9b83c73be615bd871d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tautvydas=20S=CC=8Cidlauskas?= Date: Fri, 25 Sep 2026 11:23:50 +0300 Subject: [PATCH] fix(lsp): treat files in any Flutter SDK as dependencies Detect the SDK by its bin/flutter and bin/cache/dart-sdk markers instead of path fragments, so SDKs installed via mise, asdf, puro, etc. are read-only and reuse the project's dartls client. Resolve the root from the attached buffer rather than whichever buffer is current when the async config callback runs. Fixes #534 --- lua/flutter-tools/lsp/init.lua | 13 +++++---- lua/flutter-tools/utils/path.lua | 25 +++++++++++++---- tests/path_spec.lua | 47 ++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 11 deletions(-) diff --git a/lua/flutter-tools/lsp/init.lua b/lua/flutter-tools/lsp/init.lua index a935c85..c10df59 100644 --- a/lua/flutter-tools/lsp/init.lua +++ b/lua/flutter-tools/lsp/init.lua @@ -164,15 +164,16 @@ function M.restart() end end +---@param buffer_path string? defaults to the current buffer's path ---@return string? -function M.get_project_root_dir() +function M.get_project_root_dir(buffer_path) local conf = require("flutter-tools.config") - local current_buffer_path = path.current_buffer_path() + buffer_path = buffer_path or path.current_buffer_path() -- Check if path is flutter dependency. For dependencies we do not -- search for a root directory as they are not projects. - if not path.is_flutter_dependency_path(current_buffer_path) then - local root_path = lsp_utils.is_valid_path(current_buffer_path) - and path.find_root(conf.root_patterns, current_buffer_path) + if not path.is_flutter_dependency_path(buffer_path) then + local root_path = lsp_utils.is_valid_path(buffer_path) + and path.find_root(conf.root_patterns, buffer_path) or nil if root_path ~= nil then return root_path end end @@ -284,7 +285,7 @@ function M.attach() -- This callback can run asynchronously, by which point the user may have -- switched to or deleted the buffer we were asked to attach to. if not api.nvim_buf_is_valid(buf) then return end - c.root_dir = M.get_project_root_dir() + c.root_dir = M.get_project_root_dir(buffer_path) or fs.dirname(fs.find(conf.root_patterns, { path = buffer_path, upward = true, diff --git a/lua/flutter-tools/utils/path.lua b/lua/flutter-tools/utils/path.lua index 7c6192f..def5039 100644 --- a/lua/flutter-tools/utils/path.lua +++ b/lua/flutter-tools/utils/path.lua @@ -200,14 +200,29 @@ function M.pub_cache_dir() return vim.env.HOME and M.join(vim.env.HOME, ".pub-cache") end +---@param dir string +---@return boolean +local function is_flutter_sdk_root(dir) + return M.is_file(M.join(dir, "bin", "flutter")) + and M.is_dir(M.join(dir, "bin", "cache", "dart-sdk")) +end + +---@param full_path string +---@return boolean +local function is_inside_flutter_sdk(full_path) + for dir in vim.fs.parents(full_path) do + if is_flutter_sdk_root(dir) then return true end + end + return false +end + function M.is_flutter_dependency_path(full_path) + if not full_path or full_path == "" then return false end local path_parts = { [[.pub-cache]], [[Pub\Cache]], [[/fvm/versions/]] } - if full_path then - for _, path_part in ipairs(path_parts) do - if full_path:find(path_part, nil, true) then return true end - end + for _, path_part in ipairs(path_parts) do + if full_path:find(path_part, nil, true) then return true end end - return false + return is_inside_flutter_sdk(full_path) end return M diff --git a/tests/path_spec.lua b/tests/path_spec.lua index 12e0e38..811c7a2 100644 --- a/tests/path_spec.lua +++ b/tests/path_spec.lua @@ -109,3 +109,50 @@ describe("path.pub_cache_dir", function() assert.are.equal(expected, path.pub_cache_dir()) end) end) + +describe("path.is_flutter_dependency_path", function() + local test_dir + local sdk + local project + + before_each(function() + local temp_base = vim.fn.tempname() + vim.fn.mkdir(temp_base, "p") + test_dir = vim.uv.fs_realpath(temp_base) + sdk = test_dir .. "/mise/installs/flutter/3.47.1" + project = test_dir .. "/my_app" + + vim.fn.mkdir(sdk .. "/bin/cache/dart-sdk", "p") + vim.fn.writefile({}, sdk .. "/bin/flutter") + vim.fn.mkdir(sdk .. "/packages/flutter/lib/src/widgets", "p") + vim.fn.mkdir(project .. "/lib", "p") + end) + + after_each(function() vim.fn.delete(test_dir, "rf") end) + + it("should detect files inside a Flutter SDK installed anywhere", function() + local file_path = sdk .. "/packages/flutter/lib/src/widgets/basic.dart" + assert.is_true(path.is_flutter_dependency_path(file_path)) + end) + + it("should not detect a directory without a Dart SDK as a Flutter SDK", function() + vim.fn.delete(sdk .. "/bin/cache", "rf") + local file_path = sdk .. "/packages/flutter/lib/src/widgets/basic.dart" + assert.is_false(path.is_flutter_dependency_path(file_path)) + end) + + it( + "should not detect project files", + function() assert.is_false(path.is_flutter_dependency_path(project .. "/lib/main.dart")) end + ) + + it("should detect pub cache files", function() + local file_path = "/home/user/.pub-cache/hosted/pub.dev/http-1.2.0/lib/http.dart" + assert.is_true(path.is_flutter_dependency_path(file_path)) + end) + + it("should handle missing paths", function() + assert.is_false(path.is_flutter_dependency_path(nil)) + assert.is_false(path.is_flutter_dependency_path("")) + end) +end)