diff --git a/doc/mini-files.txt b/doc/mini-files.txt index e567b2353..280ff91f1 100644 --- a/doc/mini-files.txt +++ b/doc/mini-files.txt @@ -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`. @@ -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| diff --git a/lua/mini/files.lua b/lua/mini/files.lua index 2a62808c0..66c4538cc 100644 --- a/lua/mini/files.lua +++ b/lua/mini/files.lua @@ -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`. --- @@ -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| @@ -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 } @@ -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' + 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 + 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 + + 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) + 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 = {} diff --git a/readmes/mini-files.md b/readmes/mini-files.md index aaea0bb14..fc7fe0975 100644 --- a/readmes/mini-files.md +++ b/readmes/mini-files.md @@ -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`. @@ -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: diff --git a/tests/dir-files/lsp-files/main.lua b/tests/dir-files/lsp-files/main.lua new file mode 100644 index 000000000..22bd5358b --- /dev/null +++ b/tests/dir-files/lsp-files/main.lua @@ -0,0 +1 @@ +require('something') diff --git a/tests/mock-lsp/file-ops.lua b/tests/mock-lsp/file-ops.lua new file mode 100644 index 000000000..ac2d780a9 --- /dev/null +++ b/tests/mock-lsp/file-ops.lua @@ -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') + 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() }) diff --git a/tests/screenshots/tests-test_files.lua---Preview---does-not-highlight-big-files b/tests/screenshots/tests-test_files.lua---Preview---does-not-highlight-big-files index 9b9e1b5a9..e2d14e2b1 100644 --- a/tests/screenshots/tests-test_files.lua---Preview---does-not-highlight-big-files +++ b/tests/screenshots/tests-test_files.lua---Preview---does-not-highlight-big-files @@ -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 diff --git a/tests/screenshots/tests-test_files.lua---open()---uses-icon-provider b/tests/screenshots/tests-test_files.lua---open()---uses-icon-provider index 160f8d5ce..ea1a586bb 100644 --- a/tests/screenshots/tests-test_files.lua---open()---uses-icon-provider +++ b/tests/screenshots/tests-test_files.lua---open()---uses-icon-provider @@ -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 diff --git a/tests/screenshots/tests-test_files.lua---open()---uses-icon-provider-002 b/tests/screenshots/tests-test_files.lua---open()---uses-icon-provider-002 index 160f8d5ce..ea1a586bb 100644 --- a/tests/screenshots/tests-test_files.lua---open()---uses-icon-provider-002 +++ b/tests/screenshots/tests-test_files.lua---open()---uses-icon-provider-002 @@ -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 diff --git a/tests/screenshots/tests-test_files.lua---open()---uses-icon-provider-003 b/tests/screenshots/tests-test_files.lua---open()---uses-icon-provider-003 index 5653981ed..675a8935b 100644 --- a/tests/screenshots/tests-test_files.lua---open()---uses-icon-provider-003 +++ b/tests/screenshots/tests-test_files.lua---open()---uses-icon-provider-003 @@ -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,10-8 All +15| 5,10-8 All --|---------|---------|---------|---------|---------|---------|---------|---------| 01|01111111111111111111111111100000000000000000000000000222222000000000033333333333 02|04444444455555555555555555555555555555555555555555500666666666666666077777777777 -03|04444455555555555555555555555555555555555555555555500555555555555555077777777777 -04|04444444455555555555555555555555555555555555555555500555555555555555077777777777 -05|08888886666666666666666666666666666666666666666666600555555555555555077777777777 -06|05555555555555555555555555555555555555555555555555500555555555555555077777777777 +03|04444444444455555555555555555555555555555555555555500555555555555555077777777777 +04|04444455555555555555555555555555555555555555555555500555555555555555077777777777 +05|04444444455555555555555555555555555555555555555555500555555555555555077777777777 +06|08888886666666666666666666666666666666666666666666600555555555555555077777777777 07|05555555555555555555555555555555555555555555555555500555555555555555077777777777 -08|00000000000000000000000000000000000000000000000000000000000000000000077777777777 -09|77777777777777777777777777777777777777777777777777777777777777777777777777777777 +08|05555555555555555555555555555555555555555555555555500000000000000000077777777777 +09|00000000000000000000000000000000000000000000000000007777777777777777777777777777 10|77777777777777777777777777777777777777777777777777777777777777777777777777777777 11|77777777777777777777777777777777777777777777777777777777777777777777777777777777 12|77777777777777777777777777777777777777777777777777777777777777777777777777777777 diff --git a/tests/test_files.lua b/tests/test_files.lua index 6ffde4902..a68c93a1d 100644 --- a/tests/test_files.lua +++ b/tests/test_files.lua @@ -154,7 +154,7 @@ local mock_stdpath_data = function() local data_dir = make_test_path('data') local lua_cmd = string.format( [[ - _G.stdpath_orig = vim.fn.stpath + _G.stdpath_orig = vim.fn.stdpath vim.fn.stdpath = function(what) if what == 'data' then return %s end return _G.stdpath_orig(what) @@ -406,6 +406,7 @@ T['open()']['uses icon provider'] = function() eq(get_extmarks_hl(), { 'MiniIconsAzure', 'MiniFilesDirectory', -- 'lua' directory has special highlighting + 'MiniIconsAzure', 'MiniFilesDirectory', 'MiniIconsBlue', 'MiniFilesDirectory', 'MiniIconsAzure', 'MiniFilesDirectory', 'MiniIconsAzure', 'MiniFilesDirectory', @@ -5887,6 +5888,271 @@ T['Events']['`MiniFilesActionMove` triggers'] = function() validate('dir/', true) end +T['LSP'] = new_set() + +local setup_lsp = function(file_already_opened) + if not file_already_opened then + -- Set up file + local file_path = make_test_path('lsp-files', 'main.lua') + child.cmd('edit ' .. file_path) + end + + -- Mock server + child.cmd('luafile tests/mock-lsp/file-ops.lua') + + return file_path +end + +local validate_lsp_will = function(method, files, lines) + eq(child.lua_get('_G.lsp_requests["file-methods-lsp"]'), { { method, { files = files } } }) + eq(get_lines(), lines) + child.lua('_G.lsp_requests = {}') +end + +T['LSP']['works with `willCreateFiles`'] = function() + child.lua([[ + local edit_range = { start = { line = 0, character = 0 }, ['end'] = { line = 0, character = 0 } } + _G.workspace_edits_response = { { range = edit_range, newText = '-- willCreate\n' } } + ]]) + setup_lsp() + local temp_dir = make_temp_dir('temp', {}) + + open(temp_dir) + local file_name = 'something.lua' + type_keys('C', file_name, '') + mock_confirm(1) + synchronize() + close() + local uri = vim.uri_from_fname(make_test_path('temp', file_name)) + validate_lsp_will('workspace/willCreateFiles', { { uri = uri } }, { '-- willCreate', "require('something')" }) +end + +T['LSP']['works with `willRenameFiles`'] = function() + child.lua([[ + local edit_range = { start = { line = 0, character = 9 }, ['end'] = { line = 0, character = 18 } } + _G.workspace_edits_response = { { range = edit_range, newText = 'something_else' } } + ]]) + setup_lsp() + local file_name = 'something.lua' + local temp_dir = make_temp_dir('temp', { file_name }) + + open(temp_dir) + type_keys('C', 'something_else.lua', '') + mock_confirm(1) + synchronize() + close() + local old_uri = vim.uri_from_fname(make_test_path('temp', file_name)) + local new_uri = vim.uri_from_fname(make_test_path('temp', 'something_else.lua')) + local ref_files = { { oldUri = old_uri, newUri = new_uri } } + validate_lsp_will('workspace/willRenameFiles', ref_files, { "require('something_else')" }) +end + +T['LSP']['works with `willDeleteFiles`'] = function() + child.lua([[ + local edit_range = { start = { line = 0, character = 0 }, ['end'] = { line = 0, character = 0 } } + _G.workspace_edits_response = { { range = edit_range, newText = '-- willDelete\n' } } + ]]) + setup_lsp() + local file_name = 'something.lua' + local temp_dir = make_temp_dir('temp', { file_name }) + + open(temp_dir) + type_keys('dd') + mock_confirm(1) + synchronize() + close() + local uri = vim.uri_from_fname(make_test_path('temp', file_name)) + validate_lsp_will('workspace/willDeleteFiles', { { uri = uri } }, { '-- willDelete', "require('something')" }) +end + +local validate_lsp_did = function(method, files, lines) + eq(child.lua_get('_G.lsp_notifications["file-methods-lsp"]'), { { method, { files = files } } }) + if lines ~= nil then eq(get_lines(), lines) end + child.lua('_G.lsp_notifications = {}') +end + +T['LSP']['works with `didCreateFiles`'] = function() + setup_lsp() + local temp_dir = make_temp_dir('temp', {}) + + open(temp_dir) + local file_name = 'something.lua' + type_keys('C', file_name, '') + mock_confirm(1) + synchronize() + close() + + local uri = vim.uri_from_fname(make_test_path('temp', file_name)) + validate_lsp_did('workspace/didCreateFiles', { { uri = uri } }) +end + +T['LSP']['works with `didRenameFiles`'] = function() + setup_lsp() + local file_name = 'something.lua' + local temp_dir = make_temp_dir('temp', { file_name }) + + open(temp_dir) + type_keys('C', 'something_else.lua', '') + mock_confirm(1) + synchronize() + close() + + local old_uri = vim.uri_from_fname(make_test_path('temp', file_name)) + local new_uri = vim.uri_from_fname(make_test_path('temp', 'something_else.lua')) + validate_lsp_did('workspace/didRenameFiles', { { oldUri = old_uri, newUri = new_uri } }) +end + +T['LSP']['works with `didRenameFiles` that applies workspace edit'] = function() + child.lua([[_G.did_callback = function(_, dispatchers) + local path = vim.fn.fnamemodify('tests/dir-files/lsp-files/main.lua', ':p') + local uri = vim.uri_from_fname(path) + local edit_range = { start = { line = 0, character = 9 }, ['end'] = { line = 0, character = 18 } } + local text_edit = { range = edit_range, newText = 'something_else' } + dispatchers.server_request('workspace/applyEdit', { edit = { changes = { [uri] = { text_edit } } } }) + end]]) + setup_lsp() + local file_name = 'something.lua' + local temp_dir = make_temp_dir('temp', { file_name }) + + open(temp_dir) + type_keys('C', 'something_else.lua', '') + mock_confirm(1) + synchronize() + close() + + local old_uri = vim.uri_from_fname(make_test_path('temp', file_name)) + local new_uri = vim.uri_from_fname(make_test_path('temp', 'something_else.lua')) + local ref_files = { { oldUri = old_uri, newUri = new_uri } } + validate_lsp_did('workspace/didRenameFiles', ref_files, { "require('something_else')" }) +end + +T['LSP']['works with `didRenameFiles` that applies workspace edit after confirmation'] = function() + child.lua([[_G.did_callback = function(_, dispatchers) + local msg = 'Do you want to modify the require path?' + local show_msg_params = { type = 'info', message = msg, actions = { { title = 'Confirm' } } } + local selected = dispatchers.server_request('window/showMessageRequest', show_msg_params) + if selected.title ~= 'Confirm' then return end + + local path = 'tests/dir-files/lsp-files/main.lua' + local uri = vim.uri_from_fname(vim.fn.fnamemodify(path, ':p')) + local edit_range = { start = { line = 0, character = 9 }, ['end'] = { line = 0, character = 18 } } + local text_edit = { range = edit_range, newText = 'something_else' } + dispatchers.server_request('workspace/applyEdit', { edit = { changes = { [uri] = { text_edit } } } }) + end]]) + setup_lsp() + local file_name = 'something.lua' + local temp_dir = make_temp_dir('temp', { file_name }) + + open(temp_dir) + type_keys('C', 'something_else.lua', '') + mock_confirm(1) + -- NOTE: Neovim's implementation of 'window/showMessageRequest' seems to use + -- `vim.fn.inputlist` directly here instead of `vim.ui.select` + child.lua('vim.fn.inputlist = function() return 1 end') + synchronize() + close() + + local old_uri = vim.uri_from_fname(make_test_path('temp', file_name)) + local new_uri = vim.uri_from_fname(make_test_path('temp', 'something_else.lua')) + local ref_files = { { oldUri = old_uri, newUri = new_uri } } + validate_lsp_did('workspace/didRenameFiles', ref_files, { "require('something_else')" }) +end + +T['LSP']['works with `didDeleteFiles`'] = function() + setup_lsp() + local file_name = 'something.lua' + local temp_dir = make_temp_dir('temp', { file_name }) + + open(temp_dir) + type_keys('dd') + mock_confirm(1) + synchronize() + close() + + local uri = vim.uri_from_fname(make_test_path('temp', file_name)) + validate_lsp_did('workspace/didDeleteFiles', { { uri = uri } }) +end + +T['LSP']['works with filters'] = function() + child.lua([[_G.filter_configs = { + filters = { + { pattern = { matches = 'file', glob = '**/*.lua' }, scheme = 'file' }, + { pattern = { matches = 'folder', glob = '**' }, scheme = 'file' }, + { pattern = { matches = 'file', glob = '**/{aaa,bbb}' }, scheme = 'file' }, + { pattern = { matches = 'file', glob = '**/C' }, scheme = 'file' }, + { pattern = { matches = 'file', glob = '**/D', options = { ignoreCase = true } }, scheme = 'file' }, + }, + }]]) + setup_lsp() + local temp_dir = make_temp_dir('temp', {}) + + local validate = function(file_name, should_match) + open(temp_dir) + type_keys('o', file_name, '') + mock_confirm(1) + synchronize() + close() + + local new_file_path = make_test_path('temp', file_name) + local new_file_uri = vim.uri_from_fname(new_file_path) + if file_name:match('/$') then new_file_uri = new_file_uri .. '/' end + local files = should_match and { { uri = new_file_uri } } or {} + + eq(child.lua_get('_G.lsp_requests["file-methods-lsp"]'), { { 'workspace/willCreateFiles', { files = files } } }) + child.lua('_G.lsp_requests = {}') + end + + validate('something.lua', true) + validate('something.py', false) + validate('some_dir/', true) + validate('aaa', true) + validate('bbb', true) + validate('c', false) + validate('d', true) + + if child.fn.has('fname_case') == 1 then + validate('BBB', false) + validate('D', true) + end +end + +T['LSP']['works with multiple language servers'] = function() + setup_lsp() + + child.lua([[ + _G.server_name = 'only-will-rename-lsp' + _G.file_operations_config = { willRename = { filters = { { pattern = { glob = '**' } } } } } + ]]) + setup_lsp(true) + + child.lua([[ + _G.server_name = 'only-did-rename-lsp' + _G.file_operations_config = { didRename = { filters = { { pattern = { glob = '**' } } } } } + ]]) + setup_lsp(true) + local file_name = 'something.lua' + local temp_dir = make_temp_dir('temp', { file_name }) + + open(temp_dir) + type_keys('C', 'something_else.lua', '') + mock_confirm(1) + synchronize() + close() + + local old_uri = vim.uri_from_fname(make_test_path('temp', file_name)) + local new_uri = vim.uri_from_fname(make_test_path('temp', 'something_else.lua')) + local params = { files = { { oldUri = old_uri, newUri = new_uri } } } + + eq(child.lua_get('_G.lsp_requests["file-methods-lsp"]'), { { 'workspace/willRenameFiles', params } }) + eq(child.lua_get('_G.lsp_notifications["file-methods-lsp"]'), { { 'workspace/didRenameFiles', params } }) + + eq(child.lua_get('_G.lsp_requests["only-will-rename-lsp"]'), { { 'workspace/willRenameFiles', params } }) + eq(child.lua_get('_G.lsp_notifications["only-will-rename-lsp"]'), vim.NIL) + + eq(child.lua_get('_G.lsp_requests["only-did-rename-lsp"]'), vim.NIL) + eq(child.lua_get('_G.lsp_notifications["only-did-rename-lsp"]'), { { 'workspace/didRenameFiles', params } }) +end + T['Default explorer'] = new_set() T['Default explorer']['works on startup'] = function()