Skip to content
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

feat: add FlutterRename command #234

Merged
merged 7 commits into from
Apr 9, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 needed.

<hr/>

Expand Down Expand Up @@ -273,6 +274,7 @@ require("flutter-tools").setup {
analysisExcludedFolders = {"<path-to-flutter-sdk-packages>"},
renameFilesWithClasses = "prompt", -- "always"
enableSnippets = true,
updateImportsOnRename = true, -- Whether to update imports and other directives when files are renamed. Required for `FlutterRename` command.
}
}
}
Expand Down
1 change: 1 addition & 0 deletions lua/flutter-tools.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lua/flutter-tools/lsp/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
149 changes: 149 additions & 0 deletions lua/flutter-tools/lsp/rename.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
local M = {}

local api = vim.api
local util = vim.lsp.util
local lazy = require("flutter-tools.lazy")
local path = lazy.require("flutter-tools.utils.path") ---@module "flutter-tools.utils.path"

--- Computes a filename for a given class name (convert from PascalCase to snake_case).
sidlatau marked this conversation as resolved.
Show resolved Hide resolved
local function file_name_for_class_name(class_name)
local starts_uppercase = class_name:find("^%u")
if not starts_uppercase then return nil end
sidlatau marked this conversation as resolved.
Show resolved Hide resolved
local file_name = class_name:gsub("(%u)", "_%1"):lower()
-- Removes first underscore
file_name = file_name:sub(2)
return file_name .. ".dart"
end

local function will_rename_files(old_name, new_name, callback)
sidlatau marked this conversation as resolved.
Show resolved Hide resolved
local params = vim.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 }
vim.lsp.buf_request(0, "workspace/willRenameFiles", params, function(err, result)
if err then
vim.notify(err.message or "Error on getting lsp rename results!")
return
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.
--- Function has same signature as `vim.lsp.buf.rename()` function and can be used instead of it.
function M.rename(new_name, options)
options = options or {}
local bufnr = options.bufnr or api.nvim_get_current_buf()
local clients = vim.lsp.get_active_clients({
sidlatau marked this conversation as resolved.
Show resolved Hide resolved
bufnr = bufnr,
name = "dartls",
})
local client = clients[1]
if not client then
-- Fallback to default rename function if language server is not dartls
vim.lsp.buf.rename(new_name, options)
sidlatau marked this conversation as resolved.
Show resolved Hide resolved
return
end

local win = api.nvim_get_current_win()

-- Compute early to account for cursor movements after going async
local cword = vim.fn.expand("<cword>")
local actual_file_name = vim.fn.expand("%:t")
local old_computed_filename = file_name_for_class_name(cword)
sidlatau marked this conversation as resolved.
Show resolved Hide resolved
local is_file_rename = old_computed_filename == actual_file_name

local function get_text_at_range(range, offset_encoding)
return api.nvim_buf_get_text(
bufnr,
range.start.line,
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),
akinsho marked this conversation as resolved.
Show resolved Hide resolved
{}
)[1]
end

local function rename(name, will_rename_files_result)
sidlatau marked this conversation as resolved.
Show resolved Hide resolved
local params = util.make_position_params(win, client.offset_encoding)
params.newName = name
local handler = client.handlers["textDocument/rename"]
or vim.lsp.handlers["textDocument/rename"]
client.request("textDocument/rename", params, function(...)
handler(...)
if will_rename_files_result then
-- the `will_rename_files_result` contains all the places we need to update imports
-- so we apply those edits.
vim.lsp.util.apply_workspace_edit(will_rename_files_result, client.offset_encoding)
end
end, bufnr)
end

local function rename_fix_imports(name)
akinsho marked this conversation as resolved.
Show resolved Hide resolved
if is_file_rename then
local old_file_path = vim.fn.expand("%:p")
sidlatau marked this conversation as resolved.
Show resolved Hide resolved
local new_filename = file_name_for_class_name(name)
local actual_file_head = vim.fn.expand("%:p:h")
local new_file_path = path.join(actual_file_head, new_filename)
will_rename_files(old_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 result == nil then
sidlatau marked this conversation as resolved.
Show resolved Hide resolved
local msg = err and ("Error on prepareRename: " .. (err.message or ""))
sidlatau marked this conversation as resolved.
Show resolved Hide resolved
or "Nothing to rename"
vim.notify(msg, vim.log.levels.INFO)
sidlatau marked this conversation as resolved.
Show resolved Hide resolved
return
end

if new_name then
rename_fix_imports(new_name)
sidlatau marked this conversation as resolved.
Show resolved Hide resolved
return
end

local prompt_opts = {
sidlatau marked this conversation as resolved.
Show resolved Hide resolved
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 = cword
end
vim.ui.input(prompt_opts, function(input)
sidlatau marked this conversation as resolved.
Show resolved Hide resolved
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
rename_fix_imports(new_name)
return
sidlatau marked this conversation as resolved.
Show resolved Hide resolved
end

local prompt_opts = {
prompt = "New Name: ",
default = cword,
}
vim.ui.input(prompt_opts, function(input)
if not input or #input == 0 then return end
rename_fix_imports(input)
end)
end
end

return M