Skip to content

Commit

Permalink
feat: add FlutterRename command (#234)
Browse files Browse the repository at this point in the history
  • Loading branch information
sidlatau authored Apr 9, 2023
1 parent 0a7e6b4 commit 4d9391b
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 38 deletions.
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).
---@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("<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),
{}
)[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
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

0 comments on commit 4d9391b

Please sign in to comment.