diff --git a/ftplugin/dart/init.lua b/ftplugin/dart/init.lua index a5f6442..fd2c342 100644 --- a/ftplugin/dart/init.lua +++ b/ftplugin/dart/init.lua @@ -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)" diff --git a/lua/flutter-tools.lua b/lua/flutter-tools.lua index 5890ff7..ce7d898 100644 --- a/lua/flutter-tools.lua +++ b/lua/flutter-tools.lua @@ -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 diff --git a/lua/flutter-tools/commands.lua b/lua/flutter-tools/commands.lua index e5d982e..79ee971 100644 --- a/lua/flutter-tools/commands.lua +++ b/lua/flutter-tools/commands.lua @@ -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 = {} @@ -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) @@ -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) @@ -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) @@ -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()) @@ -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 }) @@ -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 @@ -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 }) @@ -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 }) diff --git a/lua/flutter-tools/config.lua b/lua/flutter-tools/config.lua index 576d9b9..0fb3dcc 100644 --- a/lua/flutter-tools/config.lua +++ b/lua/flutter-tools/config.lua @@ -137,6 +137,7 @@ local config = { virtual_text_str = "■", background_color = nil, }, + web_port = nil }, outline = setmetatable({ auto_open = false, diff --git a/lua/flutter-tools/executable.lua b/lua/flutter-tools/executable.lua index 854cca1..1adc932 100644 --- a/lua/flutter-tools/executable.lua +++ b/lua/flutter-tools/executable.lua @@ -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 = {} @@ -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 +---@return table? 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")), @@ -62,11 +62,9 @@ end ---@type table 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?) +---@param callback fun(t: table?) ---@return table? local function path_from_lookup_cmd(lookup_cmd, callback) local paths = {} @@ -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) ---@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) } @@ -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 diff --git a/lua/flutter-tools/lsp/fvm_utils.lua b/lua/flutter-tools/lsp/fvm_utils.lua new file mode 100644 index 0000000..decb33e --- /dev/null +++ b/lua/flutter-tools/lsp/fvm_utils.lua @@ -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 diff --git a/lua/flutter-tools/lsp/init.lua b/lua/flutter-tools/lsp/init.lua index 9a1e894..b30df18 100644 --- a/lua/flutter-tools/lsp/init.lua +++ b/lua/flutter-tools/lsp/init.lua @@ -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" @@ -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 @@ -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 }, } @@ -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) @@ -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") @@ -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 diff --git a/lua/flutter-tools/lsp/utils.lua b/lua/flutter-tools/lsp/utils.lua index 30c3fd4..64291e5 100644 --- a/lua/flutter-tools/lsp/utils.lua +++ b/lua/flutter-tools/lsp/utils.lua @@ -1,14 +1,49 @@ local M = {} -local lsp = vim.lsp +local lazy = require("flutter-tools.lazy") -M.SERVER_NAME = "dartls" +local lsp = vim.lsp -- TODO: Remove after compatibility with Neovim=0.9 is dropped local get_clients = vim.fn.has("nvim-0.10") == 1 and lsp.get_clients or lsp.get_active_clients +local utils = lazy.require("flutter-tools.utils") ---@module "flutter-tools.utils" + +M.SERVER_NAME = "dartls" ---@param bufnr number? ---@return vim.lsp.Client? -function M.get_dartls_client(bufnr) return get_clients({ name = M.SERVER_NAME, bufnr = bufnr })[1] end +function M.get_dartls_client(bufnr) + local clients = get_clients({ name = M.SERVER_NAME, bufnr = bufnr }) + return utils.find(clients, function(c) return not c:is_stopped() end) +end + +function M.get_dartls_server() + local clients = get_clients({ name = M.SERVER_NAME }) + return utils.find(clients, function(c) return not c:is_stopped() end) +end + +---@param cmd string +---@return vim.lsp.Client? +function M.get_dartls_client_for_version(cmd) + local clients = get_clients({ name = M.SERVER_NAME }) + return utils.find(clients, function(c) + local isStopping = c:is_stopped() + if isStopping then return false end + local client_cmd = (c.config.cmd or {})[1] + return cmd == client_cmd + end) +end + +--- Checks if buffer path is valid for attaching LSP +--- @param buffer_path string +--- @return boolean +function M.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 return M diff --git a/lua/flutter-tools/utils/config_utils.lua b/lua/flutter-tools/utils/config_utils.lua new file mode 100644 index 0000000..551b62c --- /dev/null +++ b/lua/flutter-tools/utils/config_utils.lua @@ -0,0 +1,22 @@ +local M = {} + +local lazy = require("flutter-tools.lazy") +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 lsp = lazy.require("flutter-tools.lsp") ---@module "flutter-tools.utils" + +--- Gets the appropriate cwd +---@param project_conf flutter.ProjectConfig? +---@returns string? +function M.get_cwd(project_conf) + if project_conf and project_conf.cwd then + local resolved_path = 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_project_root_dir() +end + +return M diff --git a/lua/flutter-tools/utils/init.lua b/lua/flutter-tools/utils/init.lua index 167960d..babdf5d 100644 --- a/lua/flutter-tools/utils/init.lua +++ b/lua/flutter-tools/utils/init.lua @@ -44,8 +44,8 @@ end ---Find an item in a list based on a compare function ---@generic T ---@param compare fun(item: T): boolean ----@param list `T` ----@return `T`? +---@param list T[] +---@return T? function M.find(list, compare) for _, item in ipairs(list) do if compare(item) then return item end diff --git a/lua/flutter-tools/utils/path.lua b/lua/flutter-tools/utils/path.lua index 6e76890..42cef25 100644 --- a/lua/flutter-tools/utils/path.lua +++ b/lua/flutter-tools/utils/path.lua @@ -3,6 +3,7 @@ local lazy = require("flutter-tools.lazy") local utils = lazy.require("flutter-tools.utils") ---@module "flutter-tools.utils" local luv = vim.loop +local api = vim.api local M = {} function M.exists(filename) @@ -82,7 +83,10 @@ function M.iterate_parents(path) return it, path, path end +---@param root string? +---@param path string? function M.is_descendant(root, path) + if not root then return false end if not path then return false end local function cb(dir, _) return dir == root end @@ -113,4 +117,26 @@ function M.find_root(patterns, startpath) return M.search_ancestors(startpath, matcher) 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) + return current_buffer_path +end + +function M.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 + return M