Skip to content

feat - improve support for monorepos and fvm in monorepos #476

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
2 changes: 0 additions & 2 deletions ftplugin/dart/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
if vim.b.flutter_tools_did_ftplugin then return end
vim.b.flutter_tools_did_ftplugin = 1

require("flutter-tools.lsp").attach()

vim.opt_local.comments = [[sO:*\ -,mO:*\ \ ,exO:*/,s1:/*,mb:*,ex:*/,:///,://]]
vim.opt_local.commentstring = [[//%s]]
vim.opt.includeexpr = "v:lua.require('flutter-tools.resolve_url').resolve_url(v:fname)"
Expand Down
5 changes: 5 additions & 0 deletions lua/flutter-tools.lua
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ local function setup_autocommands()
pattern = { "*" },
callback = function() dev_tools.stop() end,
})
autocmd({ "BufReadPost", "BufFilePost", "BufEnter" }, {
group = AUGROUP,
pattern = { "*.dart" },
callback = function() lsp.attach() end,
})
end

---@param opts flutter.ProjectConfig | flutter.ProjectConfig[] Project-specific configuration
Expand Down
42 changes: 7 additions & 35 deletions lua/flutter-tools/commands.lua
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ local debugger_runner = lazy.require("flutter-tools.runners.debugger_runner") --
local path = lazy.require("flutter-tools.utils.path") ---@module "flutter-tools.utils.path"
local dev_log = lazy.require("flutter-tools.log") ---@module "flutter-tools.log"
local parser = lazy.require("flutter-tools.utils.yaml_parser")
local config_utils = lazy.require("flutter-tools.utils.config_utils") ---@module "flutter-tools.utils.config_utils"

local M = {}

Expand Down Expand Up @@ -189,34 +190,6 @@ local function get_device_from_args(args)
end
end

local function get_absolute_path(input_path)
-- Check if the provided path is an absolute path
if
vim.fn.isdirectory(input_path) == 1
and not input_path:match("^/")
and not input_path:match("^%a:[/\\]")
then
-- It's a relative path, so expand it to an absolute path
local absolute_path = vim.fn.fnamemodify(input_path, ":p")
return absolute_path
else
-- It's already an absolute path
return input_path
end
end

---@param project_conf flutter.ProjectConfig?
local function get_cwd(project_conf)
if project_conf and project_conf.cwd then
local resolved_path = get_absolute_path(project_conf.cwd)
if not vim.loop.fs_stat(resolved_path) then
return ui.notify("Provided cwd does not exist: " .. resolved_path, ui.ERROR)
end
return resolved_path
end
return lsp.get_lsp_root_dir()
end

--@return table?
local function parse_yaml(str)
local ok, yaml = pcall(parser.parse, str)
Expand Down Expand Up @@ -276,7 +249,7 @@ local function run(opts, project_conf, launch_config)
project_conf.pre_run_callback(callback_args)
end
end
local cwd = get_cwd(project_conf)
local cwd = config_utils.get_cwd(project_conf)
-- To determinate if the project is a flutter project we need to check if the pubspec.yaml
-- file has a flutter dependency in it. We need to get cwd first to pick correct pubspec.yaml file.
local is_flutter_project = has_flutter_dependency_in_pubspec(cwd)
Expand Down Expand Up @@ -333,7 +306,7 @@ local function attach(opts)
local args = opts.cli_args or opts.args or {}
if not use_debugger_runner() then table.insert(args, 1, "attach") end

local cwd = get_cwd()
local cwd = config_utils.get_cwd()
ui.notify("Attaching flutter project...")
runner = use_debugger_runner() and debugger_runner or job_runner
runner:attach(paths, args, cwd, on_run_data, on_run_exit)
Expand Down Expand Up @@ -446,7 +419,7 @@ function M.pub_get()
command = cmd,
args = { "pub", "get" },
-- stylua: ignore
cwd = lsp.get_lsp_root_dir() --[[@as string]],
cwd = lsp.get_project_root_dir() --[[@as string]],
})
pub_get_job:after_success(vim.schedule_wrap(function(j)
on_pub_get(j:result())
Expand Down Expand Up @@ -482,7 +455,7 @@ function M.pub_upgrade(cmd_args)
command = cmd,
args = args,
-- stylua: ignore
cwd = lsp.get_lsp_root_dir() --[[@as string]],
cwd = lsp.get_project_root_dir() --[[@as string]],
})
pub_upgrade_job:after_success(vim.schedule_wrap(function(j)
ui.notify(utils.join(j:result()), nil, { timeout = notify_timeout })
Expand Down Expand Up @@ -551,7 +524,6 @@ function M.fvm_use(sdk_name)
fvm_use_job:after_success(vim.schedule_wrap(function(j)
ui.notify(utils.join(j:result()))
shutdown()
executable.reset_paths()
lsp.restart()

fvm_use_job = nil
Expand Down Expand Up @@ -590,7 +562,7 @@ function M.install()
command = cmd,
args = args,
-- stylua: ignore
cwd = lsp.get_lsp_root_dir() --[[@as string]],
cwd = lsp.get_project_root_dir() --[[@as string]],
})
install_job:after_success(vim.schedule_wrap(function(j)
ui.notify(utils.join(j:result()), nil, { timeout = notify_timeout })
Expand Down Expand Up @@ -621,7 +593,7 @@ function M.uninstall()
command = cmd,
args = args,
-- stylua: ignore
cwd = lsp.get_lsp_root_dir() --[[@as string]],
cwd = lsp.get_project_root_dir() --[[@as string]],
})
uninstall_job:after_success(vim.schedule_wrap(function(j)
ui.notify(utils.join(j:result()), nil, { timeout = notify_timeout })
Expand Down
1 change: 1 addition & 0 deletions lua/flutter-tools/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ local config = {
virtual_text_str = "■",
background_color = nil,
},
web_port = nil
},
outline = setmetatable({
auto_open = false,
Expand Down
47 changes: 20 additions & 27 deletions lua/flutter-tools/executable.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ local utils = lazy.require("flutter-tools.utils") ---@module "flutter-tools.util
local path = lazy.require("flutter-tools.utils.path") ---@module "flutter-tools.utils.path"
local ui = lazy.require("flutter-tools.ui") ---@module "flutter-tools.ui"
local config = lazy.require("flutter-tools.config") ---@module "flutter-tools.config"
local fvm_utils = lazy.require("flutter-tools.lsp.fvm_utils") ---@module "flutter-tools.lsp.fvm_utils"
local Job = require("plenary.job")

local fn = vim.fn
local fs = vim.fs
local luv = vim.loop

local M = {}

Expand Down Expand Up @@ -49,9 +48,10 @@ local function _flutter_sdk_dart_bin(flutter_sdk)
end

---Get paths for flutter and dart based on the binary locations
---@return table<string, string>
---@return table<string, string>?
local function get_default_binaries()
local flutter_bin = fn.resolve(fn.exepath("flutter"))
if #flutter_bin <= 0 then return nil end
return {
flutter_bin = flutter_bin,
dart_bin = fn.resolve(fn.exepath("dart")),
Expand All @@ -62,11 +62,9 @@ end
---@type table<string, string>
local _paths = nil

function M.reset_paths() _paths = nil end

---Execute user's lookup command and pass it to the job callback
---@param lookup_cmd string
---@param callback fun(p: string, t: table<string, string>?)
---@param callback fun(t: table<string, string>?)
---@return table<string, string>?
local function path_from_lookup_cmd(lookup_cmd, callback)
local paths = {}
Expand Down Expand Up @@ -99,35 +97,29 @@ local function path_from_lookup_cmd(lookup_cmd, callback)
job:start()
end

local function _flutter_bin_from_fvm()
local fvm_root =
fs.dirname(fs.find(".fvm", { path = luv.cwd(), upward = true, type = "directory" })[1])
local binary_name = path.is_windows and "flutter.bat" or "flutter"
local flutter_bin_symlink = path.join(fvm_root, ".fvm", "flutter_sdk", "bin", binary_name)
flutter_bin_symlink = fn.exepath(flutter_bin_symlink)
local flutter_bin = luv.fs_realpath(flutter_bin_symlink)
if path.exists(flutter_bin_symlink) and path.exists(flutter_bin) then return flutter_bin end
end

---Fetch the paths to the users binaries.
---@param callback fun(paths: table<string, string>)
---@return nil
function M.get(callback)
if _paths then return callback(_paths) end
if config.fvm then
local flutter_bin = _flutter_bin_from_fvm()
if flutter_bin then
_paths = {
local fvm_root = fvm_utils.find_fvm_root()
local flutter_bin = fvm_utils.flutter_bin_from_fvm(fvm_root)
if fvm_root and flutter_bin then
-- TODO(kaerum): We currently don't cache fvm based paths
-- because we'd need a multiple entry based cache
-- that is somehow better than just traversing up
-- the directory tree looking for the nearest .fvm
local paths = {
flutter_bin = flutter_bin,
flutter_sdk = _flutter_sdk_root(flutter_bin),
fvm = true,
fvm_dir = fvm_root,
}
_paths.dart_sdk = _dart_sdk_root(_paths)
_paths.dart_bin = _flutter_sdk_dart_bin(_paths.flutter_sdk)
return callback(_paths)
paths.dart_sdk = _dart_sdk_root(paths)
paths.dart_bin = _flutter_sdk_dart_bin(paths.flutter_sdk)
return callback(paths)
end
end

if _paths then return callback(_paths) end
if config.flutter_path then
local flutter_path = fn.resolve(config.flutter_path)
_paths = { flutter_bin = flutter_path, flutter_sdk = _flutter_sdk_root(flutter_path) }
Expand All @@ -144,8 +136,9 @@ function M.get(callback)
end)
end

if not _paths then
_paths = get_default_binaries()
local paths = get_default_binaries()
if not _paths and paths then
_paths = paths
_paths.dart_sdk = _dart_sdk_root(_paths)
if _paths.flutter_sdk then _paths.dart_bin = _flutter_sdk_dart_bin(_paths.flutter_sdk) end
end
Expand Down
32 changes: 32 additions & 0 deletions lua/flutter-tools/lsp/fvm_utils.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
local M = {}

local lazy = require("flutter-tools.lazy")

local fn = vim.fn
local luv = vim.loop

local lsp_utils = lazy.require("flutter-tools.lsp.utils") ---@module "flutter-tools.lsp.utils"
local path = lazy.require("flutter-tools.utils.path") ---@module "flutter-tools.utils.path"
local config_utils = lazy.require("flutter-tools.utils.config_utils") ---@module "flutter-tools.utils.config_utils"

--- Gets the FVM root directory by traversing upwards
--- @returns string?
function M.find_fvm_root()
local current_path = path.current_buffer_path()
local search_path = lsp_utils.is_valid_path(current_path) and current_path
or config_utils.get_cwd()
return search_path and path.find_root({ ".fvm" }, search_path)
end

--- Gets the flutter binary from fvm root folder
--- @param fvm_root string fvm root folder
--- @return string?
function M.flutter_bin_from_fvm(fvm_root)
local binary_name = path.is_windows and "flutter.bat" or "flutter"
local flutter_bin_symlink = path.join(fvm_root, ".fvm", "flutter_sdk", "bin", binary_name)
flutter_bin_symlink = fn.exepath(flutter_bin_symlink)
local flutter_bin = luv.fs_realpath(flutter_bin_symlink)
if path.exists(flutter_bin_symlink) and path.exists(flutter_bin) then return flutter_bin end
end

return M
70 changes: 45 additions & 25 deletions lua/flutter-tools/lsp/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ local lsp_utils = lazy.require("flutter-tools.lsp.utils") ---@module "flutter-to
local api = vim.api
local lsp = vim.lsp
local fmt = string.format
local fs = vim.fs

local FILETYPE = "dart"

Expand Down Expand Up @@ -166,7 +165,15 @@ function M.restart()
end

---@return string?
function M.get_lsp_root_dir()
function M.get_project_root_dir()
local conf = require("flutter-tools.config")
local current_buffer_path = path.current_buffer_path()
local root_path = lsp_utils.is_valid_path(current_buffer_path)
and path.find_root(conf.root_patterns, current_buffer_path)
or nil

if root_path ~= nil then return root_path end

local client = lsp_utils.get_dartls_client()
return client and client.config.root_dir or nil
end
Expand Down Expand Up @@ -200,7 +207,7 @@ function M.dart_lsp_super()
uri = vim.uri_from_bufnr(0), -- gets URI of current buffer
},
position = {
line = lsp_line, -- 0-based line number
line = lsp_line, -- 0-based line number
character = lsp_col, -- 0-based character position
},
}
Expand All @@ -210,18 +217,25 @@ end
function M.dart_reanalyze() lsp.buf_request(0, "dart/reanalyze") end

---@param user_config table
---@param callback fun(table)
---@param callback fun(table, table)
local function get_server_config(user_config, callback)
local config = utils.merge({ name = lsp_utils.SERVER_NAME }, user_config, { "color" })
local executable = require("flutter-tools.executable")
--- TODO: if a user specifies a command we do not need to call executable.get
executable.get(function(paths)
if paths == nil then return end
local defaults = get_defaults({ flutter_sdk = paths.flutter_sdk })
local root_path = paths.dart_sdk
local debug_log = create_debug_log(user_config.debug)
debug_log(fmt("dart_sdk_path: %s", root_path))

config.cmd = config.cmd or { paths.dart_bin, "language-server", "--protocol=lsp" }
local cmd = { paths.dart_bin, "language-server", "--protocol=lsp" }

if (config.web_port) then
table.insert(cmd, "--port=" .. config.web_port)
end

config.cmd = config.cmd or cmd

config.filetypes = { FILETYPE }
config.capabilities = merge_config(defaults.capabilities, config.capabilities)
Expand All @@ -231,22 +245,12 @@ local function get_server_config(user_config, callback)
config.commands = merge_config(defaults.commands, config.commands)

config.on_init = function(client, _)
return client.notify("workspace/didChangeConfiguration", { settings = config.settings })
return client:notify("workspace/didChangeConfiguration", { settings = config.settings })
end
callback(config)
callback(config, paths)
end)
end

--- Checks if buffer path is valid for attaching LSP
local function is_valid_path(buffer_path)
if buffer_path == "" then return false end

local start_index, _, uri_prefix = buffer_path:find("^(%w+://).*")
-- Do not attach LSP if file URI prefix is not file.
-- For example LSP will not be attached for diffview:// or fugitive:// buffers.
return not start_index or uri_prefix == "file://"
end

---This was heavily inspired by nvim-metals implementation of the attach functionality
function M.attach()
local conf = require("flutter-tools.config")
Expand All @@ -255,17 +259,33 @@ function M.attach()
debug_log("attaching LSP")

local buf = api.nvim_get_current_buf()

local key = "dart_lsp_attaching"

local err, attaching = pcall(vim.api.nvim_buf_get_var, buf, key)
if (err or attaching == true) then return end
vim.api.nvim_buf_set_var(buf, key, true)

if lsp_utils.get_dartls_client(buf) ~= nil then return end

local buffer_path = api.nvim_buf_get_name(buf)

if not is_valid_path(buffer_path) then return end
if not lsp_utils.is_valid_path(buffer_path) then return end

get_server_config(user_config, function(c)
c.root_dir = M.get_lsp_root_dir()
or fs.dirname(fs.find(conf.root_patterns, {
path = buffer_path,
upward = true,
})[1])
vim.lsp.start(c)
get_server_config(user_config, function(c, paths)
local project_root = M.get_project_root_dir()
c.root_dir = paths.fvm_dir or project_root
vim.schedule(function()
local client = lsp_utils.get_dartls_client_for_version(c.cmd[1])
if client == nil then
local id = vim.lsp.start(c)
client = vim.lsp.get_clients({ id = id })[1]
if client == nil then return end
end
client:_add_workspace_folder(project_root)
vim.lsp.buf_attach_client(buf, client.id)
end)
vim.api.nvim_buf_set_var(buf, key, false)
end)
end

Expand Down
Loading
Loading