Environment
- flutter-tools.nvim
3.0.1 (0867b34)
- Neovim
0.12.5
- Flutter
3.47.1 (stable), installed via mise
- Linux
7.1.9-arch1-2
Current behavior
Opening a framework source file (e.g. after gd on StatefulWidget) starts a
dartls client rooted at the Flutter SDK itself — a 2.3 GB tree — and the
buffer is left writable.
Reproduced with stock upstream, no user config beyond flutter_lookup_cmd:
-- init.lua
local L = vim.fn.stdpath("data") .. "/lazy/"
vim.opt.rtp:append(L .. "plenary.nvim")
vim.opt.rtp:append(L .. "flutter-tools.nvim")
require("flutter-tools").setup({ flutter_lookup_cmd = "mise where flutter" })
vim.api.nvim_create_autocmd("LspAttach", {
callback = function(a)
local c = vim.lsp.get_client_by_id(a.data.client_id)
print("root=" .. tostring(c.config.root_dir),
"modifiable=" .. tostring(vim.bo[a.buf].modifiable))
end,
})
Opening <sdk>/packages/flutter/lib/src/widgets/basic.dart gives:
root = /home/user/.local/share/mise/http-tarballs/<hash>_strip_1
modifiable = true
Cause
is_flutter_dependency_path() (lua/flutter-tools/utils/path.lua:193-201)
recognises SDK/dependency locations from a hardcoded list:
local path_parts = { [[.pub-cache]], [[Pub\Cache]], [[/fvm/versions/]] }
An SDK installed by mise lives under ~/.local/share/mise/http-tarballs/...,
and one installed by asdf under ~/.asdf/installs/flutter/<version>/. Neither
matches, so a framework source file is treated as an ordinary project file and
get_project_root_dir() (lsp/init.lua:170-182) runs the upward search it was
supposed to skip.
That search uses root_patterns = { ".git", "pubspec.yaml" }, and the Flutter
SDK root contains both:
$ ls -d "$(realpath "$(mise where flutter)")"/{.git,pubspec.yaml}
.../ .../pubspec.yaml
$ du -sh "$(realpath "$(mise where flutter)")"
2.3G
so the whole SDK becomes the root.
There is a second, independent path to the same outcome:
M.attach() (lsp/init.lua:293-296) repeats the upward search whenever
get_project_root_dir() returns nil, which it does whenever no dartls client
has attached yet:
c.root_dir = M.get_project_root_dir()
or fs.dirname(fs.find(conf.root_patterns, { path = buffer_path, upward = true })[1])
So even with the dependency check corrected, this fallback can still select a
root inside the SDK (in my testing, <sdk>/packages/flutter).
Consequences
- The analysis context is the SDK rather than the user's project.
- Framework buffers stay writable.
ftplugin/dart/init.lua sets
modifiable = false through the same predicate, so the read-only protection
is silently lost for these installs (modifiable = true above).
- It is expensive. Stepping through four framework files (
framework.dart,
app_bar.dart, scaffold.dart, basic.dart) took the analysis server to
666 MB RSS at ~32% CPU in a scripted run; an interactive session reached
1.6 GB. Neovim separately received ~69 000 diagnostics across ~170 files
while only 4 buffers were loaded.
Point 3 may be inherent to onlyAnalyzeProjectsWithOpenFiles rather than a
consequence of the wrong root — I did not isolate the two, and I mention it as
practical impact rather than as a claim about the cause.
Expected behavior
A buffer inside the Flutter SDK should not produce a root_dir inside the SDK.
It should attach to the project's existing client, as already happens for
.pub-cache and fvm installs.
Suggested fix
Derive the check from the SDK path the plugin already resolves
(executable.get() → paths.flutter_sdk, compared after fs_realpath, since
version managers commonly symlink their installs) rather than from a hardcoded
fragment list. That covers every installation method without new configuration.
Guarding the or fallback in M.attach() so it cannot select a root inside the
SDK would close the second path.
Steps to reproduce
- Install Flutter via
mise (or asdf).
- Use the
init.lua above.
- Open any file under
<sdk>/packages/flutter/lib/src/.
- Observe
root_dir inside the SDK and modifiable = true.
Environment
3.0.1(0867b34)0.12.53.47.1(stable), installed viamise7.1.9-arch1-2Current behavior
Opening a framework source file (e.g. after
gdonStatefulWidget) starts adartlsclient rooted at the Flutter SDK itself — a 2.3 GB tree — and thebuffer is left writable.
Reproduced with stock upstream, no user config beyond
flutter_lookup_cmd:Opening
<sdk>/packages/flutter/lib/src/widgets/basic.dartgives:Cause
is_flutter_dependency_path()(lua/flutter-tools/utils/path.lua:193-201)recognises SDK/dependency locations from a hardcoded list:
An SDK installed by
miselives under~/.local/share/mise/http-tarballs/...,and one installed by
asdfunder~/.asdf/installs/flutter/<version>/. Neithermatches, so a framework source file is treated as an ordinary project file and
get_project_root_dir()(lsp/init.lua:170-182) runs the upward search it wassupposed to skip.
That search uses
root_patterns = { ".git", "pubspec.yaml" }, and the FlutterSDK root contains both:
so the whole SDK becomes the root.
There is a second, independent path to the same outcome:
M.attach()(lsp/init.lua:293-296) repeats the upward search wheneverget_project_root_dir()returnsnil, which it does whenever no dartls clienthas attached yet:
So even with the dependency check corrected, this fallback can still select a
root inside the SDK (in my testing,
<sdk>/packages/flutter).Consequences
ftplugin/dart/init.luasetsmodifiable = falsethrough the same predicate, so the read-only protectionis silently lost for these installs (
modifiable = trueabove).framework.dart,app_bar.dart,scaffold.dart,basic.dart) took the analysis server to666 MB RSS at ~32% CPU in a scripted run; an interactive session reached
1.6 GB. Neovim separately received ~69 000 diagnostics across ~170 files
while only 4 buffers were loaded.
Point 3 may be inherent to
onlyAnalyzeProjectsWithOpenFilesrather than aconsequence of the wrong root — I did not isolate the two, and I mention it as
practical impact rather than as a claim about the cause.
Expected behavior
A buffer inside the Flutter SDK should not produce a
root_dirinside the SDK.It should attach to the project's existing client, as already happens for
.pub-cacheandfvminstalls.Suggested fix
Derive the check from the SDK path the plugin already resolves
(
executable.get()→paths.flutter_sdk, compared afterfs_realpath, sinceversion managers commonly symlink their installs) rather than from a hardcoded
fragment list. That covers every installation method without new configuration.
Guarding the
orfallback inM.attach()so it cannot select a root inside theSDK would close the second path.
Steps to reproduce
mise(orasdf).init.luaabove.<sdk>/packages/flutter/lib/src/.root_dirinside the SDK andmodifiable = true.