Skip to content
Open
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
15 changes: 14 additions & 1 deletion doc/mini-files.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ Features:
- Opt-in preview of file or directory under cursor.

- Manipulate files and directories by editing text buffers: create, delete,
copy, rename, move. See |MiniFiles-manipulation| for overview.
rename (all three are LSP aware), copy, move.
See |MiniFiles-manipulation| for an overview.

- Use as default file explorer instead of `netrw`.

Expand Down Expand Up @@ -315,6 +316,18 @@ Note that order of text manipulation steps does not affect performed actions.

- Moving directory inside itself is not supported.

# LSP integration ~

Create, delete, and rename are LSP aware: their information is forwarded to
all active LSP servers for them to perform additional actions. This means that
LSP servers can, for example, update imports after renaming a file or populate
a file with a boilerplate code after a file creation.

The actual changes depend entirely on the LSP server and whether it supports
relevant methods:
- `workspace/will{Create,Delete,Rename}Files` before a file system action
- `workspace/did{Create,Delete,Rename}Files` after a file system action.

------------------------------------------------------------------------------
*MiniFiles-events*
To allow user customization and integration of external tools, certain |User|
Expand Down
111 changes: 110 additions & 1 deletion lua/mini/files.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
--- - Opt-in preview of file or directory under cursor.
---
--- - Manipulate files and directories by editing text buffers: create, delete,
--- copy, rename, move. See |MiniFiles-manipulation| for overview.
--- rename (all three are LSP aware), copy, move.
--- See |MiniFiles-manipulation| for an overview.
---
--- - Use as default file explorer instead of `netrw`.
---
Expand Down Expand Up @@ -310,6 +311,18 @@
--- (not icon or path index to the left of it).
---
--- - Moving directory inside itself is not supported.
---
--- # LSP integration ~
---
--- Create, delete, and rename are LSP aware: their information is forwarded to
--- all active LSP servers for them to perform additional actions. This means that
--- LSP servers can, for example, update imports after renaming a file or populate
--- a file with a boilerplate code after a file creation.
---
--- The actual changes depend entirely on the LSP server and whether it supports
--- relevant methods:
--- - `workspace/will{Create,Delete,Rename}Files` before a file system action
--- - `workspace/did{Create,Delete,Rename}Files` after a file system action.
---@tag MiniFiles-manipulation

--- To allow user customization and integration of external tools, certain |User|
Expand Down Expand Up @@ -2767,10 +2780,17 @@ H.fs_actions_to_lines = function(fs_actions)
end

H.fs_actions_apply = function(fs_actions)
H.lsp_fs_hook('willCreate', fs_actions)
H.lsp_fs_hook('willDelete', fs_actions)
H.lsp_fs_hook('willRename', fs_actions)

local ok_actions = {}
for i = 1, #fs_actions do
local diff, action = fs_actions[i], fs_actions[i].action
local ok, success = pcall(H.fs_do[action], diff.from, diff.to)
if ok and success then
table.insert(ok_actions, diff)

-- Trigger event
local to = action == 'create' and diff.to:gsub('/$', '') or diff.to
local data = { action = action, from = diff.from, to = to }
Expand All @@ -2782,6 +2802,95 @@ H.fs_actions_apply = function(fs_actions)
if has_moved then H.adjust_after_move(diff.from, to, fs_actions, i + 1) end
end
end

H.lsp_fs_hook('didCreate', ok_actions)
H.lsp_fs_hook('didDelete', ok_actions)
H.lsp_fs_hook('didRename', ok_actions)
end

H.lsp_fs_hook = function(method, diffs)
local full_method = 'workspace/' .. method .. 'Files'
Comment thread
TheLeoP marked this conversation as resolved.
local clients = vim.lsp.get_clients({ method = full_method })
if #clients == 0 then return end

-- Transform 'mini.files' diffs into LSP file actions for the input method
-- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#createFilesParams
-- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#deleteFilesParams
-- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#renameFilesParams
local files, to_uri = {}, vim.uri_from_fname
local needs_check = method == 'willCreate' or method == 'willRename'
local is_create, is_delete, is_rename =
vim.endswith(method, 'Create'), vim.endswith(method, 'Delete'), vim.endswith(method, 'Rename')
for _, d in ipairs(diffs) do
local file = {}
if is_create and d.action == 'create' then file = { uri = to_uri(d.to) } end
if is_delete and d.action == 'delete' then file = { uri = to_uri(d.from) } end
if is_rename and d.action == 'rename' then file = { oldUri = to_uri(d.from), newUri = to_uri(d.to) } end

-- Precompute LSP file type for filters (path can be not yet on disk)
file.fs_type = (d.from or d.to):find('/$') ~= nil and 'folder' or 'file'

-- Some actions might not succeed, so make best effort check before that
local pass_check = not needs_check or (needs_check and d.to ~= nil and not H.fs_is_present_path(d.to))
if (file.uri or file.oldUri) ~= nil and pass_check then table.insert(files, file) end
end

-- Execute LSP action for every currently existing client
if #files == 0 then return end
for _, client in ipairs(clients) do
Comment thread
echasnovski marked this conversation as resolved.
H.lsp_fs_hook_client(client, full_method, files)
end
end
-- TODO: Remove after compatibility with Neovim=0.9 is dropped
if vim.fn.has('nvim-0.10') == 0 then H.lsp_fs_hook = function() end end

H.lsp_fs_hook_client = function(client, full_method, lsp_files)
-- Compute parameters of the LSP action by filtering all input file actions
local is_scheme = function(uri, scheme) return scheme == nil or vim.startswith(uri, scheme .. ':') end
local is_fs_type = function(lsp_file, ref_fs_type) return ref_fs_type == nil or ref_fs_type == lsp_file.fs_type end
local make_filter = function(scheme, ref_fs_type, glob, ignore_case)
local adjust_case = ignore_case and vim.fn.tolower or function(x) return x end
local glob_lpeg = vim.glob.to_lpeg(adjust_case(glob) or '**')
return function(lsp_file)
local uri = lsp_file.uri or lsp_file.oldUri
local fname = adjust_case(vim.uri_to_fname(uri))
return is_scheme(uri, scheme) and is_fs_type(lsp_file, ref_fs_type) and glob_lpeg:match(fname) ~= nil
end
end
Comment thread
TheLeoP marked this conversation as resolved.

local method = full_method:match('^workspace/(.+)Files$')
-- https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#fileOperationFilter
local filter_configs = client.server_capabilities.workspace.fileOperations[method].filters
local filters = {}
for _, fc in ipairs(filter_configs) do
local glob, matches, scheme = fc.pattern.glob, fc.pattern.matches, fc.scheme
local ignore_case = type(fc.pattern.options) == 'table' and fc.pattern.options.ignoreCase

table.insert(filters, make_filter(scheme, matches, glob, ignore_case))
end

local params = { files = {} }
for _, f in ipairs(lsp_files) do
-- https://github.com/microsoft/language-server-protocol/issues/2203
-- Empty filters should match nothing, but this a useful default for misbehaving servers
local ok = #filters == 0
for _, filt in ipairs(filters) do
ok = ok or filt(f)
Comment thread
TheLeoP marked this conversation as resolved.
end
f.fs_type = nil
if ok then table.insert(params.files, f) end
end

-- Perform an action
if vim.startswith(method, 'did') then return client:notify(full_method, params) end
local response, err = H.client_request_sync(client, full_method, params)
if (response or {}).result ~= nil then vim.lsp.util.apply_workspace_edit(response.result, client.offset_encoding) end
end

H.client_request_sync = function(client, method, params) return client:request_sync(method, params, 1000) end
-- TODO: Remove after compatibility with Neovim=0.10 is dropped
if vim.fn.has('nvim-0.11') == 0 then
H.client_request_sync = function(client, method, params) return client.request_sync(method, params, 1000) end
end

H.fs_do = {}
Expand Down
4 changes: 3 additions & 1 deletion readmes/mini-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ https://github.com/nvim-mini/mini.nvim/assets/24854248/530483a5-fe9a-4e18-9813-a

- Opt-in preview of file or directory under cursor.

- Manipulate files and directories by editing text buffers: create, delete, copy, rename, move. See `:h MiniFiles-manipulation` for overview.
- Manipulate files and directories by editing text buffers: create, delete, rename (all three are LSP aware), copy, move. See `:h MiniFiles-manipulation` for an overview.

- Use as default file explorer instead of `netrw`.

Expand All @@ -42,6 +42,8 @@ https://github.com/nvim-mini/mini.nvim/assets/24854248/530483a5-fe9a-4e18-9813-a
- UI options: whether to show preview of file/directory under cursor, etc.
- Bookmarks for quicker navigation.

- LSP aware file operations.

See `:h MiniFiles-examples` for some common configuration examples.

Notes:
Expand Down
1 change: 1 addition & 0 deletions tests/dir-files/lsp-files/main.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require('something')
71 changes: 71 additions & 0 deletions tests/mock-lsp/file-ops.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
local server_name = _G.server_name or 'file-methods-lsp'
_G.lsp_requests = _G.lsp_requests or {}
_G.lsp_notifications = _G.lsp_notifications or {}

_G.workspace_edits_response = _G.workspace_edits_response
or { range = { start = { line = 0, character = 0 }, ['end'] = { line = 0, character = 0 } }, newText = '' }

_G.filter_configs = _G.filter_configs or { filters = { { pattern = { glob = '**' } } } }
local fc = _G.filter_configs
local file_operations_config = _G.file_operations_config
or { willCreate = fc, willDelete = fc, willRename = fc, didCreate = fc, didDelete = fc, didRename = fc }

local capabilities = { workspace = { fileOperations = file_operations_config } }

_G.did_callback = _G.did_callback or function(params, dispatchers) end

local make_will_request = function(method)
return function(params)
_G.lsp_requests[server_name] = _G.lsp_requests[server_name] or {}
table.insert(_G.lsp_requests[server_name], { method, params })
local path = vim.fn.fnamemodify('tests/dir-files/lsp-files/main.lua', ':p')
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea of separate LSP mocks is that they should not depend on module specific test files. Can this be be generalized (like by using params)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A global variable could be used for configuring this. If params is used for this, it would mean executing an edit on the created/renamed file. It would still be necessary to use something different for deleted files, though

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whatever makes it not assume that it runs in a 'mini.files' test in the most concise way.

local uri = vim.uri_from_fname(path)
return { changes = { [uri] = _G.workspace_edits_response } }
end
end

local make_did_notification = function(method)
return function(params, dispatchers)
_G.lsp_notifications[server_name] = _G.lsp_notifications[server_name] or {}
table.insert(_G.lsp_notifications[server_name], { method, params })
_G.did_callback(params, dispatchers)
end
end

local requests = {
initialize = function(_) return { capabilities = capabilities } end,
shutdown = function(_) return nil end,

['workspace/willCreateFiles'] = make_will_request('workspace/willCreateFiles'),
['workspace/willRenameFiles'] = make_will_request('workspace/willRenameFiles'),
['workspace/willDeleteFiles'] = make_will_request('workspace/willDeleteFiles'),
}

local notifications = {
['workspace/didCreateFiles'] = make_did_notification('workspace/didCreateFiles'),
['workspace/didRenameFiles'] = make_did_notification('workspace/didRenameFiles'),
['workspace/didDeleteFiles'] = make_did_notification('workspace/didDeleteFiles'),
}

local cmd = function(dispatchers)
local is_closing, request_id = false, 0

return {
request = function(method, params, callback)
local method_impl = requests[method]
if method_impl ~= nil then callback(nil, method_impl(params)) end
request_id = request_id + 1
return true, request_id
end,
notify = function(method, params)
local method_impl = notifications[method]
if method_impl ~= nil then method_impl(params, dispatchers) end
return true
end,
is_closing = function() return is_closing end,
terminate = function() is_closing = true end,
}
end

-- Start server and attach to current buffer
return vim.lsp.start({ name = server_name, cmd = cmd, root_dir = vim.fn.getcwd() })
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
--|---------|---------|---------|---------|---------|---------|---------|---------|
01|┌OCK_ROOT/tests/dir-files ┐┌ big.lua ────────────────┐
02|│ common ││local a = "aaaaaaaaaaaaaa│
03|│ lua │└─────────────────────────┘
04|│ nested
05|│ real
06|│ big.lua
07|│ init-default-explorer.l
08|│ mock-win-functions.lua
09|└─────────────────────────┘
10|~
03|│ lsp-files │└─────────────────────────┘
04|│ lua
05|│ nested
06|│ real
07|│ big.lua
08|│ init-default-explorer.l
09|│ mock-win-functions.lua │
10|└─────────────────────────┘
11|~
12|~
13|~
14|~
15| 5,9-7 All
15| 6,9-7 All

--|---------|---------|---------|---------|---------|---------|---------|---------|
01|01111111111111111111111111002222222220000000000000000033333333333333333333333333
02|04444444455555555555555555005555555555555555555555555066666666666666666666666666
03|04444455555555555555555555000000000000000000000000000066666666666666666666666666
04|04444444455555555555555555066666666666666666666666666666666666666666666666666666
05|04444445555555555555555555066666666666666666666666666666666666666666666666666666
06|07777777777777777777777777066666666666666666666666666666666666666666666666666666
07|05555555555555555555555555066666666666666666666666666666666666666666666666666666
03|04444444444455555555555555000000000000000000000000000066666666666666666666666666
04|04444455555555555555555555066666666666666666666666666666666666666666666666666666
05|04444444455555555555555555066666666666666666666666666666666666666666666666666666
06|04444445555555555555555555066666666666666666666666666666666666666666666666666666
07|07777777777777777777777777066666666666666666666666666666666666666666666666666666
08|05555555555555555555555555066666666666666666666666666666666666666666666666666666
09|00000000000000000000000000066666666666666666666666666666666666666666666666666666
10|66666666666666666666666666666666666666666666666666666666666666666666666666666666
09|05555555555555555555555555066666666666666666666666666666666666666666666666666666
10|00000000000000000000000000066666666666666666666666666666666666666666666666666666
11|66666666666666666666666666666666666666666666666666666666666666666666666666666666
12|66666666666666666666666666666666666666666666666666666666666666666666666666666666
13|66666666666666666666666666666666666666666666666666666666666666666666666666666666
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
--|---------|---------|---------|---------|---------|---------|---------|---------|
01|┌MOCK_ROOT/tests/dir-files ────────────────────────┐┌ real ─────────┐
02|│󰉋 common ││󰢱 a.lua │
03|│󰉋 lua ││󰦪 b.txt │
04|│󰉋 nested ││󰵸 c.gif │
05|│󰉋 real ││ LICENSE │
06|│󰢱 init-default-explorer.lua ││󱁤 Makefile │
07|│󰢱 mock-win-functions.lua ││󰈔 top-secret │
08|└──────────────────────────────────────────────────┘└───────────────┘
09|~
03|│󰉋 lsp-files ││󰦪 b.txt │
04|│󰉋 lua ││󰵸 c.gif │
05|│󰉋 nested ││ LICENSE │
06|│󰉋 real ││󱁤 Makefile │
07|│󰢱 init-default-explorer.lua ││󰈔 top-secret │
08|│󰢱 mock-win-functions.lua │└───────────────┘
09|└──────────────────────────────────────────────────┘
10|~
11|~
12|~
13|~
14|~
15| 4,11-8 All
15| 5,11-8 All

--|---------|---------|---------|---------|---------|---------|---------|---------|
01|01111111111111111111111111100000000000000000000000000222222000000000033333333333
02|04444444455555555555555555555555555555555555555555500667777777777777088888888888
03|09944455555555555555555555555555555555555555555555500::5555555555555088888888888
04|04444444455555555555555555555555555555555555555555500445555555555555088888888888
05|06666667777777777777777777777777777777777777777777700;;5555555555555088888888888
06|04455555555555555555555555555555555555555555555555500<<5555555555555088888888888
03|04444444444455555555555555555555555555555555555555500995555555555555088888888888
04|0::44455555555555555555555555555555555555555555555500445555555555555088888888888
05|04444444455555555555555555555555555555555555555555500;;5555555555555088888888888
06|06666667777777777777777777777777777777777777777777700<<5555555555555088888888888
07|04455555555555555555555555555555555555555555555555500<<5555555555555088888888888
08|00000000000000000000000000000000000000000000000000000000000000000000088888888888
09|88888888888888888888888888888888888888888888888888888888888888888888888888888888
08|04455555555555555555555555555555555555555555555555500000000000000000088888888888
09|00000000000000000000000000000000000000000000000000008888888888888888888888888888
10|88888888888888888888888888888888888888888888888888888888888888888888888888888888
11|88888888888888888888888888888888888888888888888888888888888888888888888888888888
12|88888888888888888888888888888888888888888888888888888888888888888888888888888888
Expand Down
Loading
Loading