From 5e401b693b929da36d08943d4fda0191fe12f02c Mon Sep 17 00:00:00 2001 From: Mathias Fussenegger Date: Mon, 15 Mar 2021 22:34:22 +0100 Subject: [PATCH 1/3] lsp: Add support for file rename via workspaceEdit --- runtime/lua/vim/lsp/protocol.lua | 3 ++ runtime/lua/vim/lsp/util.lua | 33 +++++++++++++- test/functional/plugin/lsp_spec.lua | 68 +++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 2 deletions(-) diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 0f440d6d70..187aad7684 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -749,6 +749,9 @@ function protocol.make_client_capabilities() }; workspaceFolders = true; applyEdit = true; + workspaceEdit = { + resourceOperations = {'rename',}, + }; }; callHierarchy = { dynamicRegistration = false; diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 4b528d7090..601a6ae8ca 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -612,6 +612,29 @@ 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 + + --- Applies a `WorkspaceEdit`. --- --@param workspace_edit (table) `WorkspaceEdit` @@ -619,8 +642,14 @@ 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 then + -- TODO(ashkan) handle CreateFile/DeleteFile error(string.format("Unsupported change: %q", vim.inspect(change))) else M.apply_text_document_edit(change, idx) diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index c62d91cb6d..2fd9934e7d 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -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. @@ -1309,6 +1311,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 = { From 191afb42be90c3f39f445c8340632e732e6710f8 Mon Sep 17 00:00:00 2001 From: Mathias Fussenegger Date: Thu, 18 Mar 2021 11:07:06 +0100 Subject: [PATCH 2/3] lsp: Add support for create workspaceEdit resource operation --- runtime/lua/vim/lsp/protocol.lua | 2 +- runtime/lua/vim/lsp/util.lua | 16 ++++++++- test/functional/plugin/lsp_spec.lua | 54 +++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 187aad7684..085ca7b4a1 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -750,7 +750,7 @@ function protocol.make_client_capabilities() workspaceFolders = true; applyEdit = true; workspaceEdit = { - resourceOperations = {'rename',}, + resourceOperations = {'rename', 'create',}, }; }; callHierarchy = { diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 601a6ae8ca..82fd77ea90 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -635,6 +635,18 @@ function M.rename(old_fname, new_fname, opts) 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 + + --- Applies a `WorkspaceEdit`. --- --@param workspace_edit (table) `WorkspaceEdit` @@ -648,8 +660,10 @@ function M.apply_workspace_edit(workspace_edit) vim.uri_to_fname(change.newUri), change.options ) + elseif change.kind == 'create' then + create_file(change) elseif change.kind then - -- TODO(ashkan) handle CreateFile/DeleteFile + -- TODO(ashkan) handle DeleteFile error(string.format("Unsupported change: %q", vim.inspect(change))) else M.apply_text_document_edit(change, idx) diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index 2fd9934e7d..eb8f5e8824 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -1265,6 +1265,60 @@ 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) end) describe('completion_list_to_complete_items', function() From 84213b5b9adfaaf133771432ca468cea8faa4cc9 Mon Sep 17 00:00:00 2001 From: Mathias Fussenegger Date: Thu, 18 Mar 2021 11:32:33 +0100 Subject: [PATCH 3/3] lsp: Add support for delete workspaceEdit resource operation --- runtime/lua/vim/lsp/protocol.lua | 2 +- runtime/lua/vim/lsp/util.lua | 24 +++++++++++++++++- test/functional/plugin/lsp_spec.lua | 39 +++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 085ca7b4a1..7e43eb84de 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -750,7 +750,7 @@ function protocol.make_client_capabilities() workspaceFolders = true; applyEdit = true; workspaceEdit = { - resourceOperations = {'rename', 'create',}, + resourceOperations = {'rename', 'create', 'delete',}, }; }; callHierarchy = { diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 82fd77ea90..6945d0f1ec 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -647,6 +647,27 @@ local function create_file(change) 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` @@ -662,8 +683,9 @@ function M.apply_workspace_edit(workspace_edit) ) elseif change.kind == 'create' then create_file(change) + elseif change.kind == 'delete' then + delete_file(change) elseif change.kind then - -- TODO(ashkan) handle DeleteFile error(string.format("Unsupported change: %q", vim.inspect(change))) else M.apply_text_document_edit(change, idx) diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index eb8f5e8824..6d3af115fa 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -1319,6 +1319,45 @@ describe('LSP', function() 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()