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)