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 all 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 `lsp.settings.renameFilesWithClasses == "always"`

<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
11 changes: 6 additions & 5 deletions lua/flutter-tools/lsp/color/init.lua
Original file line number Diff line number Diff line change
@@ -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

Expand Down
14 changes: 5 additions & 9 deletions lua/flutter-tools/lsp/commands.lua
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -24,22 +27,15 @@ 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.
-- see more: https://github.com/dart-lang/sdk/blob/e995cb5f7cd67d39c1ee4bdbe95c8241db36725f/pkg/analysis_server/lib/src/lsp/handlers/commands/perform_refactor.dart#L53
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
32 changes: 8 additions & 24 deletions lua/flutter-tools/lsp/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ 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
local fmt = string.format
local fs = vim.fs

local FILETYPE = "dart"
local SERVER_NAME = "dartls"
local ROOT_PATTERNS = { ".git", "pubspec.yaml" }

local M = {
Expand Down 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 Expand Up @@ -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()
Expand All @@ -130,39 +128,25 @@ 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

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
Expand All @@ -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)
Expand Down
150 changes: 150 additions & 0 deletions lua/flutter-tools/lsp/rename.lua
Original file line number Diff line number Diff line change
@@ -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).
sidlatau marked this conversation as resolved.
Show resolved Hide resolved
---@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)
sidlatau marked this conversation as resolved.
Show resolved Hide resolved
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("<cword>")
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),
akinsho marked this conversation as resolved.
Show resolved Hide resolved
{}
)[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)
akinsho marked this conversation as resolved.
Show resolved Hide resolved
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
13 changes: 13 additions & 0 deletions lua/flutter-tools/lsp/utils.lua
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions lua/flutter-tools/ui.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down