Merge pull request #14144 from mfussenegger/lsp-workspace-edit-rename

lsp: Add support for create,rename,delete workspaceEdit resourceOperations
This commit is contained in:
Michael Lingelbach 2021-03-18 12:27:16 -07:00 committed by GitHub
commit f31b8dabfd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 231 additions and 2 deletions

View File

@ -749,6 +749,9 @@ function protocol.make_client_capabilities()
};
workspaceFolders = true;
applyEdit = true;
workspaceEdit = {
resourceOperations = {'rename', 'create', 'delete',},
};
};
callHierarchy = {
dynamicRegistration = false;

View File

@ -612,6 +612,62 @@ function M.text_document_completion_list_to_complete_items(result, prefix)
return matches
end
--- Rename old_fname to new_fname
--
--@param opts (table)
-- overwrite? bool
-- ignoreIfExists? bool
function M.rename(old_fname, new_fname, opts)
opts = opts or {}
local bufnr = vim.fn.bufadd(old_fname)
vim.fn.bufload(bufnr)
local target_exists = vim.loop.fs_stat(new_fname) ~= nil
if target_exists and not opts.overwrite or opts.ignoreIfExists then
vim.notify('Rename target already exists. Skipping rename.')
return
end
local ok, err = os.rename(old_fname, new_fname)
assert(ok, err)
api.nvim_buf_call(bufnr, function()
vim.cmd('saveas! ' .. vim.fn.fnameescape(new_fname))
end)
end
local function create_file(change)
local opts = change.options or {}
-- from spec: Overwrite wins over `ignoreIfExists`
local fname = vim.uri_to_fname(change.uri)
if not opts.ignoreIfExists or opts.overwrite then
local file = io.open(fname, 'w')
file:close()
end
vim.fn.bufadd(fname)
end
local function delete_file(change)
local opts = change.options or {}
local fname = vim.uri_to_fname(change.uri)
local stat = vim.loop.fs_stat(fname)
if opts.ignoreIfNotExists and not stat then
return
end
assert(stat, "Cannot delete not existing file or folder " .. fname)
local flags
if stat and stat.type == 'directory' then
flags = opts.recursive and 'rf' or 'd'
else
flags = ''
end
local bufnr = vim.fn.bufadd(fname)
local result = tonumber(vim.fn.delete(fname, flags))
assert(result == 0, 'Could not delete file: ' .. fname .. ', stat: ' .. vim.inspect(stat))
api.nvim_buf_delete(bufnr, { force = true })
end
--- Applies a `WorkspaceEdit`.
---
--@param workspace_edit (table) `WorkspaceEdit`
@ -619,8 +675,17 @@ end
function M.apply_workspace_edit(workspace_edit)
if workspace_edit.documentChanges then
for idx, change in ipairs(workspace_edit.documentChanges) do
if change.kind then
-- TODO(ashkan) handle CreateFile/RenameFile/DeleteFile
if change.kind == "rename" then
M.rename(
vim.uri_to_fname(change.oldUri),
vim.uri_to_fname(change.newUri),
change.options
)
elseif change.kind == 'create' then
create_file(change)
elseif change.kind == 'delete' then
delete_file(change)
elseif change.kind then
error(string.format("Unsupported change: %q", vim.inspect(change)))
else
M.apply_text_document_edit(change, idx)

View File

@ -11,6 +11,8 @@ local pesc = helpers.pesc
local insert = helpers.insert
local retry = helpers.retry
local NIL = helpers.NIL
local read_file = require('test.helpers').read_file
local write_file = require('test.helpers').write_file
-- Use these to get access to a coroutine so that I can run async tests and use
-- yield.
@ -1263,6 +1265,99 @@ describe('LSP', function()
return vim.api.nvim_buf_get_lines(target_bufnr, 0, -1, false)
]], make_workspace_edit(edits), target_bufnr))
end)
it('Supports file creation with CreateFile payload', function()
local tmpfile = helpers.tmpname()
os.remove(tmpfile) -- Should not exist, only interested in a tmpname
local uri = exec_lua('return vim.uri_from_fname(...)', tmpfile)
local edit = {
documentChanges = {
{
kind = 'create',
uri = uri,
},
}
}
exec_lua('vim.lsp.util.apply_workspace_edit(...)', edit)
eq(true, exec_lua('return vim.loop.fs_stat(...) ~= nil', tmpfile))
end)
it('createFile does not touch file if it exists and ignoreIfExists is set', function()
local tmpfile = helpers.tmpname()
write_file(tmpfile, 'Dummy content')
local uri = exec_lua('return vim.uri_from_fname(...)', tmpfile)
local edit = {
documentChanges = {
{
kind = 'create',
uri = uri,
options = {
ignoreIfExists = true,
},
},
}
}
exec_lua('vim.lsp.util.apply_workspace_edit(...)', edit)
eq(true, exec_lua('return vim.loop.fs_stat(...) ~= nil', tmpfile))
eq('Dummy content', read_file(tmpfile))
end)
it('createFile overrides file if overwrite is set', function()
local tmpfile = helpers.tmpname()
write_file(tmpfile, 'Dummy content')
local uri = exec_lua('return vim.uri_from_fname(...)', tmpfile)
local edit = {
documentChanges = {
{
kind = 'create',
uri = uri,
options = {
overwrite = true,
ignoreIfExists = true, -- overwrite must win over ignoreIfExists
},
},
}
}
exec_lua('vim.lsp.util.apply_workspace_edit(...)', edit)
eq(true, exec_lua('return vim.loop.fs_stat(...) ~= nil', tmpfile))
eq('', read_file(tmpfile))
end)
it('DeleteFile delete file and buffer', function()
local tmpfile = helpers.tmpname()
write_file(tmpfile, 'Be gone')
local uri = exec_lua([[
local fname = select(1, ...)
local bufnr = vim.fn.bufadd(fname)
vim.fn.bufload(bufnr)
return vim.uri_from_fname(fname)
]], tmpfile)
local edit = {
documentChanges = {
{
kind = 'delete',
uri = uri,
}
}
}
eq(true, pcall(exec_lua, 'vim.lsp.util.apply_workspace_edit(...)', edit))
eq(false, exec_lua('return vim.loop.fs_stat(...) ~= nil', tmpfile))
eq(false, exec_lua('return vim.api.nvim_buf_is_loaded(vim.fn.bufadd(...))', tmpfile))
end)
it('DeleteFile fails if file does not exist and ignoreIfNotExists is false', function()
local tmpfile = helpers.tmpname()
os.remove(tmpfile)
local uri = exec_lua('return vim.uri_from_fname(...)', tmpfile)
local edit = {
documentChanges = {
{
kind = 'delete',
uri = uri,
options = {
ignoreIfNotExists = false,
}
}
}
}
eq(false, pcall(exec_lua, 'vim.lsp.util.apply_workspace_edit(...)', edit))
eq(false, exec_lua('return vim.loop.fs_stat(...) ~= nil', tmpfile))
end)
end)
describe('completion_list_to_complete_items', function()
@ -1309,6 +1404,72 @@ describe('LSP', function()
end)
end)
describe('lsp.util.rename', function()
it('Can rename an existing file', function()
local old = helpers.tmpname()
write_file(old, 'Test content')
local new = helpers.tmpname()
os.remove(new) -- only reserve the name, file must not exist for the test scenario
local lines = exec_lua([[
local old = select(1, ...)
local new = select(2, ...)
vim.lsp.util.rename(old, new)
-- after rename the target file must have the contents of the source file
local bufnr = vim.fn.bufadd(new)
return vim.api.nvim_buf_get_lines(bufnr, 0, -1, true)
]], old, new)
eq({'Test content'}, lines)
local exists = exec_lua('return vim.loop.fs_stat(...) ~= nil', old)
eq(false, exists)
exists = exec_lua('return vim.loop.fs_stat(...) ~= nil', new)
eq(true, exists)
os.remove(new)
end)
it('Does not rename file if target exists and ignoreIfExists is set or overwrite is false', function()
local old = helpers.tmpname()
write_file(old, 'Old File')
local new = helpers.tmpname()
write_file(new, 'New file')
exec_lua([[
local old = select(1, ...)
local new = select(2, ...)
vim.lsp.util.rename(old, new, { ignoreIfExists = true })
]], old, new)
eq(true, exec_lua('return vim.loop.fs_stat(...) ~= nil', old))
eq('New file', read_file(new))
exec_lua([[
local old = select(1, ...)
local new = select(2, ...)
vim.lsp.util.rename(old, new, { overwrite = false })
]], old, new)
eq(true, exec_lua('return vim.loop.fs_stat(...) ~= nil', old))
eq('New file', read_file(new))
end)
it('Does override target if overwrite is true', function()
local old = helpers.tmpname()
write_file(old, 'Old file')
local new = helpers.tmpname()
write_file(new, 'New file')
exec_lua([[
local old = select(1, ...)
local new = select(2, ...)
vim.lsp.util.rename(old, new, { overwrite = true })
]], old, new)
eq(false, exec_lua('return vim.loop.fs_stat(...) ~= nil', old))
eq(true, exec_lua('return vim.loop.fs_stat(...) ~= nil', new))
eq('Old file\n', read_file(new))
end)
end)
describe('lsp.util.locations_to_items', function()
it('Convert Location[] to items', function()
local expected = {