diff --git a/README.md b/README.md index 20f7bd9e..b3fe665d 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,7 @@ require("flutter-tools").setup {} -- use defaults - `FlutterLspRestart` - This command restarts the dart language server, and is intended for situations where it begins to work incorrectly. - `FlutterSuper` - Go to super class, method using custom LSP method `dart/textDocument/super`. - `FlutterReanalyze` - Forces LSP server reanalyze using custom LSP method `dart/reanalyze`. +- `FlutterRename` - Renames and updates imports if `lsp.settings.renameFilesWithClasses == "always"`
@@ -273,6 +274,7 @@ require("flutter-tools").setup { analysisExcludedFolders = {""}, renameFilesWithClasses = "prompt", -- "always" enableSnippets = true, + updateImportsOnRename = true, -- Whether to update imports and other directives when files are renamed. Required for `FlutterRename` command. } } } diff --git a/lua/flutter-tools.lua b/lua/flutter-tools.lua index 98a429ee..91fe9c13 100644 --- a/lua/flutter-tools.lua +++ b/lua/flutter-tools.lua @@ -48,6 +48,7 @@ local function setup_commands() --- LSP command("FlutterSuper", lsp.dart_lsp_super) command("FlutterReanalyze", lsp.dart_reanalyze) + command("FlutterRename", function() require("flutter-tools.lsp.rename").rename() end) end ---Initialise various plugin modules diff --git a/lua/flutter-tools/lsp/color/init.lua b/lua/flutter-tools/lsp/color/init.lua index 988bfbe8..746dbbdd 100644 --- a/lua/flutter-tools/lsp/color/init.lua +++ b/lua/flutter-tools/lsp/color/init.lua @@ -1,15 +1,16 @@ local M = {} +local lazy = require("flutter-tools.lazy") +local lsp_utils = lazy.require("flutter-tools.lsp.utils") ---@module "flutter-tools.lsp.utils" + function M.document_color() local params = { textDocument = vim.lsp.util.make_text_document_params(), } - local clients = vim.lsp.get_active_clients({ name = "dartls" }) - for _, client in ipairs(clients) do - if client.server_capabilities.colorProvider then - client.request("textDocument/documentColor", params, nil, 0) - end + local client = lsp_utils.get_dartls_client() + if client and client.server_capabilities.colorProvider then + client.request("textDocument/documentColor", params, nil, 0) end end diff --git a/lua/flutter-tools/lsp/commands.lua b/lua/flutter-tools/lsp/commands.lua index 687c4118..22a7c977 100644 --- a/lua/flutter-tools/lsp/commands.lua +++ b/lua/flutter-tools/lsp/commands.lua @@ -1,5 +1,8 @@ local M = {} +local lazy = require("flutter-tools.lazy") +local ui = lazy.require("flutter-tools.ui") ---@module "flutter-tools.ui" + function M.refactor_perform(command, ctx) local client = vim.lsp.get_client_by_id(ctx.client_id) @@ -24,8 +27,7 @@ function M.refactor_perform(command, ctx) prompt = prompt, default = default, } - - local on_confirm = function(name) + ui.input(opts, function(name) if not name then return end -- The 6th argument is the additional options of the refactor command. -- For the extract method/local variable/widget commands, we can specify an optional `name` option. @@ -33,13 +35,7 @@ function M.refactor_perform(command, ctx) local optionsIndex = 6 command.arguments[optionsIndex] = { name = name } client.request("workspace/executeCommand", command) - end - if vim.ui and vim.ui.input then - vim.ui.input(opts, on_confirm) - else - local input = vim.fn.input(opts) - if #input > 0 then on_confirm(input) end - end + end) end return M diff --git a/lua/flutter-tools/lsp/init.lua b/lua/flutter-tools/lsp/init.lua index df073240..6ae15ffa 100644 --- a/lua/flutter-tools/lsp/init.lua +++ b/lua/flutter-tools/lsp/init.lua @@ -2,6 +2,7 @@ local lazy = require("flutter-tools.lazy") local utils = lazy.require("flutter-tools.utils") ---@module "flutter-tools.utils" local path = lazy.require("flutter-tools.utils.path") ---@module "flutter-tools.utils.path" local color = lazy.require("flutter-tools.lsp.color") ---@module "flutter-tools.lsp.color" +local lsp_utils = lazy.require("flutter-tools.lsp.utils") ---@module "flutter-tools.lsp.utils" local api = vim.api local lsp = vim.lsp @@ -9,7 +10,6 @@ local fmt = string.format local fs = vim.fs local FILETYPE = "dart" -local SERVER_NAME = "dartls" local ROOT_PATTERNS = { ".git", "pubspec.yaml" } local M = { @@ -75,6 +75,7 @@ local function get_defaults(opts) path.join(flutter_sdk_path, "packages"), path.join(flutter_sdk_path, ".pub-cache"), }, + updateImportsOnRename = true, }, }, handlers = { @@ -116,10 +117,7 @@ local function get_defaults(opts) end function M.restart() - local client = utils.find( - vim.lsp.get_active_clients(), - function(client) return client.name == SERVER_NAME end - ) + local client = lsp_utils.get_dartls_client() if client then local bufs = lsp.get_buffers_by_client_id(client.id) client.stop() @@ -130,31 +128,17 @@ function M.restart() end end ----@param server_name string? ----@return lsp.Client? -local function get_dartls_client(server_name) - server_name = server_name or SERVER_NAME - return lsp.get_active_clients({ name = server_name })[1] -end - ---@return string? function M.get_lsp_root_dir() - local client = get_dartls_client() + local client = lsp_utils.get_dartls_client() return client and client.config.root_dir or nil end -- FIXME: I'm not sure how to correctly wait till a server is ready before -- sending this request. Ideally we would wait till the server is ready. M.document_color = function() - local active_clients = vim.tbl_map(function(c) return c.id end, vim.lsp.get_active_clients()) - local dartls = get_dartls_client() - if - dartls - and vim.tbl_contains(active_clients, dartls.id) - and dartls.server_capabilities.colorProvider - then - color.document_color() - end + local client = lsp_utils.get_dartls_client() + if client and client.server_capabilities.colorProvider then color.document_color() end end M.on_document_color = color.on_document_color @@ -162,7 +146,7 @@ function M.dart_lsp_super() local conf = require("flutter-tools.config") local user_config = conf.lsp local debug_log = create_debug_log(user_config.debug) - local client = get_dartls_client() + local client = lsp_utils.get_dartls_client() if client == nil then debug_log("No active dartls server found") return @@ -175,7 +159,7 @@ function M.dart_reanalyze() lsp.buf_request(0, "dart/reanalyze") end ---@param user_config table ---@param callback fun(table) local function get_server_config(user_config, callback) - local config = utils.merge({ name = SERVER_NAME }, user_config, { "color" }) + 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) diff --git a/lua/flutter-tools/lsp/rename.lua b/lua/flutter-tools/lsp/rename.lua new file mode 100644 index 00000000..2198bfc4 --- /dev/null +++ b/lua/flutter-tools/lsp/rename.lua @@ -0,0 +1,150 @@ +local M = {} + +local lazy = require("flutter-tools.lazy") +local config = lazy.require("flutter-tools.config") ---@module "flutter-tools.config" +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 ui = lazy.require("flutter-tools.ui") ---@module "flutter-tools.ui" + +local api = vim.api +local util = vim.lsp.util +local lsp = vim.lsp +local fn = vim.fn +local fs = vim.fs + +--- Computes a filename for a given class name (convert from PascalCase to snake_case). +---@param class_name string +---@return string? +local function convert_to_file_name(class_name) + local starts_uppercase = class_name:find("^%u") + if not starts_uppercase then return end + local file_name = class_name:gsub("(%u)", "_%1"):lower() + -- Removes first underscore + file_name = file_name:sub(2) + return file_name .. ".dart" +end + +---@param old_name string +---@param new_name string +---@param callback function +local function will_rename_files(old_name, new_name, callback) + local params = lsp.util.make_position_params() + if not new_name then return end + local file_change = { + newUri = vim.uri_from_fname(new_name), + oldUri = vim.uri_from_fname(old_name), + } + params.files = { file_change } + lsp.buf_request(0, "workspace/willRenameFiles", params, function(err, result) + if err then + return ui.notify(err.message or "Error on getting lsp rename results!", ui.ERROR) + end + callback(result) + end) +end + +--- Call this function when you want rename class or anything else. +--- If file will be renamed too, this function will update imports. +--- This is a modificated version of `vim.lsp.buf.rename()` function and can be used instead of it. +--- Original version: https://github.com/neovim/neovim/blob/0bc323850410df4c3c1dd8fabded9d2000189270/runtime/lua/vim/lsp/buf.lua#L271 +function M.rename(new_name, options) + options = options or {} + local bufnr = options.bufnr or api.nvim_get_current_buf() + local client = lsp_utils.get_dartls_client(bufnr) + if not client or config.lsp.settings.renameFilesWithClasses ~= "always" then + -- Fallback to default rename function if language server is not dartls + -- or if user doesn't want to rename files on class rename. + return lsp.buf.rename(new_name, options) + end + + local win = api.nvim_get_current_win() + + -- Compute early to account for cursor movements after going async + local word_under_cursor = fn.expand("") + local current_file_path = api.nvim_buf_get_name(bufnr) + local current_file_name = fs.basename(current_file_path) + local filename_from_class_name = convert_to_file_name(word_under_cursor) + local is_file_rename = filename_from_class_name == current_file_name + + ---@param range table + ---@param offset_encoding string + local function get_text_at_range(range, offset_encoding) + return api.nvim_buf_get_text( + bufnr, + range.start.line, + -- Private method that may be not stable. + -- Source in case of changes: https://github.com/neovim/neovim/blob/0bc323850410df4c3c1dd8fabded9d2000189270/runtime/lua/vim/lsp/util.lua#L2152 + util._get_line_byte_from_position(bufnr, range.start, offset_encoding), + range["end"].line, + util._get_line_byte_from_position(bufnr, range["end"], offset_encoding), + {} + )[1] + end + + ---@param name string the name of the thing + ---@param result table | nil the result from the call to will rename + local function rename(name, result) + local params = util.make_position_params(win, client.offset_encoding) + params.newName = name + local handler = client.handlers["textDocument/rename"] or lsp.handlers["textDocument/rename"] + client.request("textDocument/rename", params, function(...) + handler(...) + if result then lsp.util.apply_workspace_edit(result, client.offset_encoding) end + end, bufnr) + end + + ---@param name string + local function rename_fix_imports(name) + if is_file_rename then + local new_filename = convert_to_file_name(name) + local new_file_path = path.join(fs.dirname(current_file_path), new_filename) + + will_rename_files(current_file_path, new_file_path, function(result) rename(name, result) end) + else + rename(name) + end + end + + if client.supports_method("textDocument/prepareRename") then + local params = util.make_position_params(win, client.offset_encoding) + client.request("textDocument/prepareRename", params, function(err, result) + if err or not result then + local msg = err and ("Error on prepareRename: %s"):format(err.message) + or "Nothing to rename" + return ui.notify(msg, ui.INFO) + end + + if new_name then return rename_fix_imports(new_name) end + + local prompt_opts = { prompt = "New Name: " } + -- result: Range | { range: Range, placeholder: string } + if result.placeholder then + prompt_opts.default = result.placeholder + elseif result.start then + prompt_opts.default = get_text_at_range(result, client.offset_encoding) + elseif result.range then + prompt_opts.default = get_text_at_range(result.range, client.offset_encoding) + else + prompt_opts.default = word_under_cursor + end + ui.input(prompt_opts, function(input) + if not input or #input == 0 then return end + rename_fix_imports(input) + end) + end, bufnr) + else + assert(client.supports_method("textDocument/rename"), "Client must support textDocument/rename") + if new_name then return rename_fix_imports(new_name) end + + local prompt_opts = { + prompt = "New Name: ", + default = word_under_cursor, + } + ui.input(prompt_opts, function(input) + if not input or #input == 0 then return end + rename_fix_imports(input) + end) + end +end + +return M diff --git a/lua/flutter-tools/lsp/utils.lua b/lua/flutter-tools/lsp/utils.lua new file mode 100644 index 00000000..3933174a --- /dev/null +++ b/lua/flutter-tools/lsp/utils.lua @@ -0,0 +1,13 @@ +local M = {} + +local lsp = vim.lsp + +M.SERVER_NAME = "dartls" + +---@param bufnr number? +---@return lsp.Client? +function M.get_dartls_client(bufnr) + return lsp.get_active_clients({ name = M.SERVER_NAME, bufnr = bufnr })[1] +end + +return M diff --git a/lua/flutter-tools/ui.lua b/lua/flutter-tools/ui.lua index 5111503d..4cac3918 100644 --- a/lua/flutter-tools/ui.lua +++ b/lua/flutter-tools/ui.lua @@ -74,6 +74,10 @@ M.notify = function(msg, level, opts) }) end +---@param opts table +---@param on_confirm function +M.input = function(opts, on_confirm) vim.ui.input(opts, on_confirm) end + --- @param items SelectionEntry[] --- @param title string --- @param on_select fun(item: SelectionEntry)