Skip to content

Commit c183be1

Browse files
committed
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
1 parent 539e84f commit c183be1

File tree

3 files changed

+147
-3
lines changed

3 files changed

+147
-3
lines changed

lua/flutter-tools/decorations.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ local fn, api = vim.fn, vim.api
1313
---Asynchronously read the data in the pubspec yaml and pass the results to a callback
1414
---@param callback fun(data: string):nil
1515
local function read_pubspec(callback)
16-
local root_patterns = { ".git", "pubspec.yaml" }
16+
local conf = require("flutter-tools.config")
1717
local current_dir = fn.expand("%:p:h")
18-
local root_dir = path.find_root(root_patterns, current_dir) or current_dir
18+
local root_dir = path.find_root(conf.root_patterns, current_dir) or current_dir
1919
local pubspec_path = path.join(root_dir, "pubspec.yaml")
2020
local pubspec = Path:new(pubspec_path)
2121
pubspec:read(callback)

lua/flutter-tools/utils/path.lua

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ function M.search_ancestors(startpath, func)
108108
end
109109
end
110110

111-
function M.find_root(patterns, startpath)
111+
local function find_nearest_root(patterns, startpath)
112112
local function matcher(path)
113113
for _, pattern in ipairs(patterns) do
114114
if M.exists(vim.fn.glob(M.join(path, pattern))) then return path end
@@ -117,6 +117,65 @@ function M.find_root(patterns, startpath)
117117
return M.search_ancestors(startpath, matcher)
118118
end
119119

120+
---@param pubspec_path string
121+
---@return table|nil
122+
local function parse_pubspec(pubspec_path)
123+
if not M.is_file(pubspec_path) then return nil end
124+
local content = vim.fn.readfile(pubspec_path)
125+
if not content or #content == 0 then return nil end
126+
local joined_content = table.concat(content, "\n")
127+
local ok, parsed = pcall(function()
128+
return require("flutter-tools.utils.yaml_parser").parse(joined_content)
129+
end)
130+
if ok and parsed then return parsed end
131+
return nil
132+
end
133+
134+
--- Checks for `resolution: workspace` in pubspec.yaml
135+
---@param pubspec_path string
136+
---@return boolean
137+
local function is_pub_workspace_member(pubspec_path)
138+
local pubspec = parse_pubspec(pubspec_path)
139+
if not pubspec then return false end
140+
return pubspec.resolution == "workspace"
141+
end
142+
143+
--- Checks for `workspace:` field in pubspec.yaml
144+
---@param pubspec_path string
145+
---@return boolean
146+
local function is_pub_workspace_root(pubspec_path)
147+
local pubspec = parse_pubspec(pubspec_path)
148+
if not pubspec then return false end
149+
return pubspec.workspace ~= nil
150+
end
151+
152+
--- Find project root, traversing up to workspace root if in a pub workspace
153+
---@param patterns string[]
154+
---@param startpath string
155+
---@return string|nil
156+
function M.find_root(patterns, startpath)
157+
local root = find_nearest_root(patterns, startpath)
158+
if not root then return nil end
159+
160+
local pubspec_path = M.join(root, "pubspec.yaml")
161+
if not is_pub_workspace_member(pubspec_path) then
162+
return root
163+
end
164+
165+
-- Workspace member, traverse upward to find the workspace root
166+
local parent = M.dirname(root)
167+
if not parent or parent == root then return root end
168+
169+
for dir in M.iterate_parents(parent) do
170+
local workspace_pubspec = M.join(dir, "pubspec.yaml")
171+
if is_pub_workspace_root(workspace_pubspec) then
172+
return dir
173+
end
174+
end
175+
176+
return root
177+
end
178+
120179
function M.current_buffer_path()
121180
local current_buffer = api.nvim_get_current_buf()
122181
local current_buffer_path = api.nvim_buf_get_name(current_buffer)

tests/path_spec.lua

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
local path = require("flutter-tools.utils.path")
2+
3+
describe("path.find_root", function()
4+
local test_dir
5+
local workspace_root
6+
local package_a
7+
local package_b
8+
local standalone
9+
10+
before_each(function()
11+
-- Use realpath to normalize (handles /var -> /private/var symlink on macOS)
12+
local temp_base = vim.fn.tempname()
13+
vim.fn.mkdir(temp_base, "p")
14+
test_dir = vim.loop.fs_realpath(temp_base)
15+
workspace_root = test_dir .. "/workspace"
16+
package_a = workspace_root .. "/packages/package_a"
17+
package_b = workspace_root .. "/packages/package_b"
18+
standalone = test_dir .. "/standalone"
19+
20+
vim.fn.mkdir(package_a, "p")
21+
vim.fn.mkdir(package_b, "p")
22+
vim.fn.mkdir(standalone, "p")
23+
24+
vim.fn.writefile({
25+
"name: my_workspace",
26+
"workspace:",
27+
" - packages/package_a",
28+
" - packages/package_b",
29+
}, workspace_root .. "/pubspec.yaml")
30+
31+
vim.fn.writefile({
32+
"name: package_a",
33+
"resolution: workspace",
34+
}, package_a .. "/pubspec.yaml")
35+
36+
vim.fn.writefile({
37+
"name: package_b",
38+
"resolution: workspace",
39+
}, package_b .. "/pubspec.yaml")
40+
41+
vim.fn.writefile({
42+
"name: standalone",
43+
"version: 1.0.0",
44+
}, standalone .. "/pubspec.yaml")
45+
end)
46+
47+
after_each(function()
48+
vim.fn.delete(test_dir, "rf")
49+
end)
50+
51+
local patterns = { "pubspec.yaml" }
52+
53+
it("should find workspace root from member package", function()
54+
local file_path = package_a .. "/lib/main.dart"
55+
vim.fn.mkdir(package_a .. "/lib", "p")
56+
vim.fn.writefile({ "void main() {}" }, file_path)
57+
58+
assert.are.equal(workspace_root, path.find_root(patterns, file_path))
59+
end)
60+
61+
it("should find workspace root from nested directory", function()
62+
local nested_dir = package_b .. "/lib/src/widgets"
63+
vim.fn.mkdir(nested_dir, "p")
64+
local file_path = nested_dir .. "/button.dart"
65+
vim.fn.writefile({ "class Button {}" }, file_path)
66+
67+
assert.are.equal(workspace_root, path.find_root(patterns, file_path))
68+
end)
69+
70+
it("should return package root for non-workspace package", function()
71+
local file_path = standalone .. "/lib/main.dart"
72+
vim.fn.mkdir(standalone .. "/lib", "p")
73+
vim.fn.writefile({ "void main() {}" }, file_path)
74+
75+
assert.are.equal(standalone, path.find_root(patterns, file_path))
76+
end)
77+
78+
it("should return workspace root when starting from workspace root", function()
79+
local file_path = workspace_root .. "/tool/script.dart"
80+
vim.fn.mkdir(workspace_root .. "/tool", "p")
81+
vim.fn.writefile({ "void main() {}" }, file_path)
82+
83+
assert.are.equal(workspace_root, path.find_root(patterns, file_path))
84+
end)
85+
end)

0 commit comments

Comments
 (0)