Skip to content

Commit

Permalink
fix: LSP renaming did not work in some cases
Browse files Browse the repository at this point in the history
There were some edge cases that caused issues when renaming files with
LSP servers. Hopefully fix all of them by using the implementation
that's also shared by neotree and nvim-tree:
https://github.com/antosha417/nvim-lsp-file-operations

Thanks @chaozwn for suggesting the fix!

Closes <#80>

This work was started in
#190 where the idea is to
add a dependency on nvim-lsp-file-operations, but that PR has become
slightly stalled for now.

This PR is a temporary fix to this stalling by simply embedding the new
package inside yazi.nvim for now. When the issues in the PR have been
resolved, the embedding can be removed.
  • Loading branch information
mikavilpas committed Jul 25, 2024
1 parent a197e9a commit ac990c1
Show file tree
Hide file tree
Showing 19 changed files with 515 additions and 108 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/typecheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ jobs:
level: Information
libraries:
# space separated
# TODO enable later
# https://github.com/antosha417/nvim-lsp-file-operations
https://github.com/nvim-lua/plenary.nvim
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ COLOR_WHITE := \033[1;37m
# install development and testing dependencies
init:
luarocks init --no-gitignore
luarocks build --local --only-deps --dev
luarocks install busted 2.2.0-1

@echo ""
Expand Down
2 changes: 1 addition & 1 deletion integration-tests/cypress/e2e/lazy-loading.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ describe("lazy loading yazi.nvim", () => {
// NOTE: if this number changes in the future, it's ok. This test is just
// to make sure that we don't accidentally load all modules up front due to
// an unrelated change.
cy.contains("Loaded 4 modules")
cy.contains("Loaded 6 modules")
})
})
11 changes: 11 additions & 0 deletions lazy.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@
return {
{ 'nvim-lua/plenary.nvim', lazy = true },
{ 'akinsho/bufferline.nvim', lazy = true },

--
-- TODO enable after https://github.com/nvim-neorocks/nvim-busted-action/issues/4 is resolved
--
-- {
-- -- Neovim plugin that adds support for file operations using built-in LSP
-- -- https://github.com/antosha417/nvim-lsp-file-operations
-- 'antosha417/nvim-lsp-file-operations',
-- lazy = true,
-- },

{
'mikavilpas/yazi.nvim',
---@type YaziConfig
Expand Down
4 changes: 4 additions & 0 deletions lua/yazi.lua
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ function M.setup(opts)
local Log = require('yazi.log')
Log.level = M.config.log_level

pcall(function()
require('yazi.lsp.embedded.lsp-file-operations').setup()
end)

local yazi_augroup = vim.api.nvim_create_augroup('yazi', { clear = true })

if M.config.open_for_directories == true then
Expand Down
11 changes: 6 additions & 5 deletions lua/yazi/log.lua
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,14 @@ function Log:write_message(level, message)
end
end

---@param level yazi.LogLevel
function Log:active_for_level(level)
return self.level and self.level ~= log_levels.OFF and self.level <= level
end

---@param message string
function Log:debug(message)
if
self.level
and self.level ~= log_levels.OFF
and self.level <= log_levels.DEBUG
then
if self:active_for_level(log_levels.DEBUG) then
self:write_message('DEBUG', message)
end
end
Expand Down
57 changes: 7 additions & 50 deletions lua/yazi/lsp/delete.lua
Original file line number Diff line number Diff line change
@@ -1,57 +1,14 @@
local M = {}

---@param path string
local function notify_file_was_deleted(path)
-- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_willDeleteFiles
local method = 'workspace/willDeleteFiles'

local clients = vim.lsp.get_clients({
method = method,
bufnr = vim.api.nvim_get_current_buf(),
})

for _, client in ipairs(clients) do
local resp = client.request_sync(method, {
files = {
{
uri = vim.uri_from_fname(path),
},
},
}, 1000, 0)

if resp and resp.result ~= nil then
vim.lsp.util.apply_workspace_edit(resp.result, client.offset_encoding)
end
end
end

---@param path string
local function notify_delete_complete(path)
-- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_didDeleteFiles
local method = 'workspace/didDeleteFiles'
local will_delete = require('yazi.lsp.embedded.lsp-file-operations.will-delete')
local did_delete = require('yazi.lsp.embedded.lsp-file-operations.did-delete')

local clients = vim.lsp.get_clients({
method = method,
bufnr = vim.api.nvim_get_current_buf(),
})

for _, client in ipairs(clients) do
-- NOTE: this returns nothing, so no need to do anything with the response
client.request_sync(method, {
files = {
{
uri = vim.uri_from_fname(path),
},
},
}, 1000, 0)
end
end
local M = {}

-- Send a notification to LSP servers, letting them know that yazi just deleted some files
-- Send a notification to LSP servers, letting them know that yazi just deleted
-- some files. Execute any changes that the LSP says are needed in other files.
---@param path string
function M.file_deleted(path)
notify_file_was_deleted(path)
notify_delete_complete(path)
will_delete.callback({ fname = path })
did_delete.callback({ fname = path })
end

return M
Empty file added lua/yazi/lsp/embedded.lua
Empty file.
147 changes: 147 additions & 0 deletions lua/yazi/lsp/embedded/lsp-file-operations.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
local M = {}

local log = require('yazi.lsp.embedded.lsp-file-operations.log')

local default_config = {
debug = false,
timeout_ms = 10000,
operations = {
willRenameFiles = true,
didRenameFiles = true,
willCreateFiles = true,
didCreateFiles = true,
willDeleteFiles = true,
didDeleteFiles = true,
},
}

local modules = {
willRenameFiles = 'yazi.lsp.embedded.lsp-file-operations.will-rename',
didRenameFiles = 'yazi.lsp.embedded.lsp-file-operations.did-rename',
willCreateFiles = 'yazi.lsp.embedded.lsp-file-operations.will-create',
didCreateFiles = 'yazi.lsp.embedded.lsp-file-operations.did-create',
willDeleteFiles = 'yazi.lsp.embedded.lsp-file-operations.will-delete',
didDeleteFiles = 'yazi.lsp.embedded.lsp-file-operations.did-delete',
}

local capabilities = {
willRenameFiles = 'willRename',
didRenameFiles = 'didRename',
willCreateFiles = 'willCreate',
didCreateFiles = 'didCreate',
willDeleteFiles = 'willDelete',
didDeleteFiles = 'didDelete',
}

---@alias HandlerMap table<string, string[]> a mapping from modules to events that trigger it

--- helper function to subscribe events to a given module callback
---@param op_events HandlerMap the table that maps modules to event strings
---@param subscribe fun(module: string, event: string) the function for how to subscribe a module to an event
local function setup_events(op_events, subscribe)
for operation, enabled in pairs(M.config.operations) do
if enabled then
local module, events = modules[operation], op_events[operation]
if module and events then
vim.tbl_map(function(event)
subscribe(module, event)
end, events)
end
end
end
end

M.setup = function(opts)
M.config = vim.tbl_deep_extend('force', default_config, opts or {})
if M.config.debug then
log.level = 'debug'
end

-- nvim-tree integration
local ok_nvim_tree, nvim_tree_api = pcall(require, 'nvim-tree.api')
if ok_nvim_tree then
log.debug('Setting up nvim-tree integration')

---@type HandlerMap
local nvim_tree_event = nvim_tree_api.events.Event
local events = {
willRenameFiles = { nvim_tree_event.WillRenameNode },
didRenameFiles = { nvim_tree_event.NodeRenamed },
willCreateFiles = { nvim_tree_event.WillCreateFile },
didCreateFiles = {
nvim_tree_event.FileCreated,
nvim_tree_event.FolderCreated,
},
willDeleteFiles = { nvim_tree_event.WillRemoveFile },
didDeleteFiles = {
nvim_tree_event.FileRemoved,
nvim_tree_event.FolderRemoved,
},
}
setup_events(events, function(module, event)
nvim_tree_api.events.subscribe(event, function(args)
require(module).callback(args)
end)
end)
end

-- neo-tree integration
local ok_neo_tree, neo_tree_events = pcall(require, 'neo-tree.events')
if ok_neo_tree then
log.debug('Setting up neo-tree integration')

---@type HandlerMap
local events = {
willRenameFiles = {
neo_tree_events.BEFORE_FILE_RENAME,
neo_tree_events.BEFORE_FILE_MOVE,
},
didRenameFiles = {
neo_tree_events.FILE_RENAMED,
neo_tree_events.FILE_MOVED,
},
didCreateFiles = { neo_tree_events.FILE_ADDED },
didDeleteFiles = { neo_tree_events.FILE_DELETED },
-- currently no events in neo-tree for before creating or deleting, so unable to support those file operations
-- Issue to add the missing events: https://github.com/nvim-neo-tree/neo-tree.nvim/issues/1276
}
setup_events(events, function(module, event)
-- create an event name based on the module and the event
local id = ('%s.%s'):format(module, event)
-- just in case setup is called twice, unsubscribe from event
neo_tree_events.unsubscribe({ id = id })
neo_tree_events.subscribe({
id = id,
event = event,
handler = function(args)
-- translate neo-tree arguemnts to the same format as nvim-tree
if type(args) == 'table' then
args = { old_name = args.source, new_name = args.destination }
else
args = { fname = args }
end
-- load module and call the callback
require(module).callback(args)
end,
})
end)
log.debug('Neo-tree integration setup complete')
end
end

--- The extra client capabilities provided by this plugin. To be merged with
--- vim.lsp.protocol.make_client_capabilities() and sent to the LSP server.
M.default_capabilities = function()
local config = M.config or default_config
local result = {
workspace = {
fileOperations = {},
},
}
for operation, capability in pairs(capabilities) do
result.workspace.fileOperations[capability] = config.operations[operation]
end
return result
end

return M
27 changes: 27 additions & 0 deletions lua/yazi/lsp/embedded/lsp-file-operations/did-create.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
local utils = require('yazi.lsp.embedded.lsp-file-operations.utils')
local log = require('yazi.lsp.embedded.lsp-file-operations.log')

local M = {}

M.callback = function(data)
for _, client in pairs(vim.lsp.get_active_clients()) do
local did_create = utils.get_nested_path(
client,
{ 'server_capabilities', 'workspace', 'fileOperations', 'didCreate' }
)
if did_create ~= nil then
local filters = did_create.filters or {}
if utils.matches_filters(filters, data.fname) then
local params = {
files = {
{ uri = vim.uri_from_fname(data.fname) },
},
}
client.notify('workspace/didCreateFiles', params)
log.debug('Sending workspace/didCreateFiles notification', params)
end
end
end
end

return M
27 changes: 27 additions & 0 deletions lua/yazi/lsp/embedded/lsp-file-operations/did-delete.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
local utils = require('yazi.lsp.embedded.lsp-file-operations.utils')
local log = require('yazi.lsp.embedded.lsp-file-operations.log')

local M = {}

M.callback = function(data)
for _, client in pairs(vim.lsp.get_active_clients()) do
local did_delete = utils.get_nested_path(
client,
{ 'server_capabilities', 'workspace', 'fileOperations', 'didDelete' }
)
if did_delete ~= nil then
local filters = did_delete.filters or {}
if utils.matches_filters(filters, data.fname) then
local params = {
files = {
{ uri = vim.uri_from_fname(data.fname) },
},
}
client.notify('workspace/didDeleteFiles', params)
log.debug('Sending workspace/didDeleteFiles notification', params)
end
end
end
end

return M
30 changes: 30 additions & 0 deletions lua/yazi/lsp/embedded/lsp-file-operations/did-rename.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
local utils = require('yazi.lsp.embedded.lsp-file-operations.utils')
local log = require('yazi.lsp.embedded.lsp-file-operations.log')

local M = {}

M.callback = function(data)
for _, client in pairs(vim.lsp.get_active_clients()) do
local did_rename = utils.get_nested_path(
client,
{ 'server_capabilities', 'workspace', 'fileOperations', 'didRename' }
)
if did_rename ~= nil then
local filters = did_rename.filters or {}
if utils.matches_filters(filters, data.old_name) then
local params = {
files = {
{
oldUri = vim.uri_from_fname(data.old_name),
newUri = vim.uri_from_fname(data.new_name),
},
},
}
client.notify('workspace/didRenameFiles', params)
log.debug('Sending workspace/didRenameFiles notification', params)
end
end
end
end

return M
5 changes: 5 additions & 0 deletions lua/yazi/lsp/embedded/lsp-file-operations/log.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
local log = require('plenary.log')

return log.new({
plugin = 'nvim-yazi.lsp.embedded.lsp-file-operations',
}, false)
Loading

0 comments on commit ac990c1

Please sign in to comment.