diff --git a/lua/oil/lsp/_types.lua b/lua/oil/lsp/_types.lua new file mode 100644 index 0000000..f4603fb --- /dev/null +++ b/lua/oil/lsp/_types.lua @@ -0,0 +1,224 @@ +-- LSP types copied from Neovim core to make typechecking pass + +---@alias lsp.null nil +---@alias uinteger integer +---@alias lsp.decimal number +---@alias lsp.DocumentUri string +---@alias lsp.URI string +---@alias lsp.LSPObject table +---@alias lsp.LSPArray lsp.LSPAny[] +---@alias lsp.LSPAny lsp.LSPObject|lsp.LSPArray|string|number|boolean|nil + +---An identifier to refer to a change annotation stored with a workspace edit. +---@alias lsp.ChangeAnnotationIdentifier string + +---A pattern kind describing if a glob pattern matches a file a folder or +---both. +--- +---@since 3.16.0 +---@alias lsp.FileOperationPatternKind +---| "file" # file +---| "folder" # folder + +---Matching options for the file operation pattern. +--- +---@since 3.16.0 +---@class lsp.FileOperationPatternOptions +---The pattern should be matched ignoring casing. +---@field ignoreCase? boolean + +---Describes textual changes on a text document. A TextDocumentEdit describes all changes +---on a document version Si and after they are applied move the document to version Si+1. +---So the creator of a TextDocumentEdit doesn't need to sort the array of edits or do any +---kind of ordering. However the edits must be non overlapping. +---@class lsp.TextDocumentEdit +---The text document to change. +---@field textDocument lsp.OptionalVersionedTextDocumentIdentifier +---The edits to be applied. +--- +---@since 3.16.0 - support for AnnotatedTextEdit. This is guarded using a +---client capability. +---@field edits lsp.TextEdit|lsp.AnnotatedTextEdit[] + +---A literal to identify a text document in the client. +---@class lsp.TextDocumentIdentifier +---The text document's uri. +---@field uri lsp.DocumentUri + +---A text document identifier to optionally denote a specific version of a text document. +---@class lsp.OptionalVersionedTextDocumentIdentifier: lsp.TextDocumentIdentifier +---The version number of this document. If a versioned text document identifier +---is sent from the server to the client and the file is not open in the editor +---(the server has not received an open notification before) the server can send +---`null` to indicate that the version is unknown and the content on disk is the +---truth (as specified with document content ownership). +---@field version integer|lsp.null + +---A special text edit with an additional change annotation. +--- +---@since 3.16.0. +---@class lsp.AnnotatedTextEdit: lsp.TextEdit +---The actual identifier of the change annotation +---@field annotationId lsp.ChangeAnnotationIdentifier + +---A workspace edit represents changes to many resources managed in the workspace. The edit +---should either provide `changes` or `documentChanges`. If documentChanges are present +---they are preferred over `changes` if the client can handle versioned document edits. +--- +---Since version 3.13.0 a workspace edit can contain resource operations as well. If resource +---operations are present clients need to execute the operations in the order in which they +---are provided. So a workspace edit for example can consist of the following two changes: +---(1) a create file a.txt and (2) a text document edit which insert text into file a.txt. +--- +---An invalid sequence (e.g. (1) delete file a.txt and (2) insert text into file a.txt) will +---cause failure of the operation. How the client recovers from the failure is described by +---the client capability: `workspace.workspaceEdit.failureHandling` +---@class lsp.WorkspaceEdit +---Holds changes to existing resources. +---@field changes? table +---Depending on the client capability `workspace.workspaceEdit.resourceOperations` document changes +---are either an array of `TextDocumentEdit`s to express changes to n different text documents +---where each text document edit addresses a specific version of a text document. Or it can contain +---above `TextDocumentEdit`s mixed with create, rename and delete file / folder operations. +--- +---Whether a client supports versioned document edits is expressed via +---`workspace.workspaceEdit.documentChanges` client capability. +--- +---If a client neither supports `documentChanges` nor `workspace.workspaceEdit.resourceOperations` then +---only plain `TextEdit`s using the `changes` property are supported. +---@field documentChanges? (lsp.TextDocumentEdit|lsp.CreateFile|lsp.RenameFile|lsp.DeleteFile)[] +---A map of change annotations that can be referenced in `AnnotatedTextEdit`s or create, rename and +---delete file / folder operations. +--- +---Whether clients honor this property depends on the client capability `workspace.changeAnnotationSupport`. +--- +---@since 3.16.0 +---@field changeAnnotations? table + +---Additional information that describes document changes. +--- +---@since 3.16.0 +---@class lsp.ChangeAnnotation +---A human-readable string describing the actual change. The string +---is rendered prominent in the user interface. +---@field label string +---A flag which indicates that user confirmation is needed +---before applying the change. +---@field needsConfirmation? boolean +---A human-readable string which is rendered less prominent in +---the user interface. +---@field description? string + +---A text edit applicable to a text document. +---@class lsp.TextEdit +---The range of the text document to be manipulated. To insert +---text into a document create a range where start === end. +---@field range lsp.Range +---The string to be inserted. For delete operations use an +---empty string. +---@field newText string + +---Options to create a file. +---@class lsp.CreateFileOptions +---Overwrite existing file. Overwrite wins over `ignoreIfExists` +---@field overwrite? boolean +---Ignore if exists. +---@field ignoreIfExists? boolean + +---Rename file options +---@class lsp.RenameFileOptions +---Overwrite target if existing. Overwrite wins over `ignoreIfExists` +---@field overwrite? boolean +---Ignores if target exists. +---@field ignoreIfExists? boolean + +---Delete file options +---@class lsp.DeleteFileOptions +---Delete the content recursively if a folder is denoted. +---@field recursive? boolean +---Ignore the operation if the file doesn't exist. +---@field ignoreIfNotExists? boolean + +---A generic resource operation. +---@class lsp.ResourceOperation +---The resource operation kind. +---@field kind string +---An optional annotation identifier describing the operation. +--- +---@since 3.16.0 +---@field annotationId? lsp.ChangeAnnotationIdentifier + +---Create file operation. +---@class lsp.CreateFile: lsp.ResourceOperation +---A create +---@field kind "create" +---The resource to create. +---@field uri lsp.DocumentUri +---Additional options +---@field options? lsp.CreateFileOptions + +---Rename file operation +---@class lsp.RenameFile: lsp.ResourceOperation +---A rename +---@field kind "rename" +---The old (existing) location. +---@field oldUri lsp.DocumentUri +---The new location. +---@field newUri lsp.DocumentUri +---Rename options. +---@field options? lsp.RenameFileOptions + +---Delete file operation +---@class lsp.DeleteFile: lsp.ResourceOperation +---A delete +---@field kind "delete" +---The file to delete. +---@field uri lsp.DocumentUri +---Delete options. +---@field options? lsp.DeleteFileOptions + +---A pattern to describe in which file operation requests or notifications +---the server is interested in receiving. +--- +---@since 3.16.0 +---@class lsp.FileOperationPattern +---The glob pattern to match. Glob patterns can have the following syntax: +---- `*` to match one or more characters in a path segment +---- `?` to match on one character in a path segment +---- `**` to match any number of path segments, including none +---- `{}` to group sub patterns into an OR expression. (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) +---- `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) +---- `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) +---@field glob string +---Whether to match files or folders with this pattern. +--- +---Matches both if undefined. +---@field matches? lsp.FileOperationPatternKind +---Additional options used during matching. +---@field options? lsp.FileOperationPatternOptions + +---A filter to describe in which file operation requests or notifications +---the server is interested in receiving. +--- +---@since 3.16.0 +---@class lsp.FileOperationFilter +---A Uri scheme like `file` or `untitled`. +---@field scheme? string +---The actual file operation pattern. +---@field pattern lsp.FileOperationPattern + +--- @class vim.lpeg.Pattern +--- @operator unm: vim.lpeg.Pattern +--- @operator add(vim.lpeg.Pattern): vim.lpeg.Pattern +--- @operator sub(vim.lpeg.Pattern): vim.lpeg.Pattern +--- @operator mul(vim.lpeg.Pattern): vim.lpeg.Pattern +--- @operator mul(vim.lpeg.Capture): vim.lpeg.Pattern +--- @operator div(string): vim.lpeg.Capture +--- @operator div(number): vim.lpeg.Capture +--- @operator div(table): vim.lpeg.Capture +--- @operator div(function): vim.lpeg.Capture +--- @operator pow(number): vim.lpeg.Pattern +--- @operator mod(function): nil +--- @field match fun(pattern: vim.lpeg.Pattern, subject: string, init?: integer): integer|vim.lpeg.Capture|nil + +--- @alias vim.lpeg.Capture vim.lpeg.Pattern diff --git a/lua/oil/lsp/helpers.lua b/lua/oil/lsp/helpers.lua new file mode 100644 index 0000000..e0a1451 --- /dev/null +++ b/lua/oil/lsp/helpers.lua @@ -0,0 +1,110 @@ +local config = require("oil.config") +local util = require("oil.util") +local workspace = require("oil.lsp.workspace") + +local M = {} + +---@param actions oil.Action[] +---@return fun() did_perform Call this function when the file operations have been completed +M.will_perform_file_operations = function(actions) + local moves = {} + local creates = {} + local deletes = {} + for _, action in ipairs(actions) do + if action.type == "move" then + local src_scheme, src_path = util.parse_url(action.src_url) + assert(src_path) + local src_adapter = assert(config.get_adapter_by_scheme(src_scheme)) + local dest_scheme, dest_path = util.parse_url(action.dest_url) + local dest_adapter = assert(config.get_adapter_by_scheme(dest_scheme)) + if src_adapter.name == "files" and dest_adapter.name == "files" then + moves[src_path] = dest_path + end + elseif action.type == "create" then + local scheme, path = util.parse_url(action.url) + local adapter = assert(config.get_adapter_by_scheme(scheme)) + if adapter.name == "files" then + table.insert(creates, path) + end + elseif action.type == "delete" then + local scheme, path = util.parse_url(action.url) + local adapter = assert(config.get_adapter_by_scheme(scheme)) + if adapter.name == "files" then + table.insert(deletes, path) + end + elseif action.type == "copy" then + local scheme, path = util.parse_url(action.dest_url) + local adapter = assert(config.get_adapter_by_scheme(scheme)) + if adapter.name == "files" then + table.insert(creates, path) + end + end + end + + local buf_was_modified = {} + for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do + buf_was_modified[bufnr] = vim.bo[bufnr].modified + end + + local edited_uris = {} + local final_err = nil + ---@param edits nil|{edit: lsp.WorkspaceEdit, client_offset: string}[] + local function accum(edits, err) + final_err = final_err or err + if edits then + for _, edit in ipairs(edits) do + if edit.edit.changes then + for uri in pairs(edit.edit.changes) do + edited_uris[uri] = true + end + end + if edit.edit.documentChanges then + for _, change in ipairs(edit.edit.documentChanges) do + if change.textDocument then + edited_uris[change.textDocument.uri] = true + end + end + end + end + end + end + accum(workspace.will_create_files(creates)) + accum(workspace.will_delete_files(deletes)) + accum(workspace.will_rename_files(moves)) + if final_err then + vim.notify( + string.format("[lsp] file operation error: %s", vim.inspect(final_err)), + vim.log.levels.WARN + ) + end + + return function() + workspace.did_create_files(creates) + workspace.did_delete_files(deletes) + workspace.did_rename_files(moves) + + local autosave = config.lsp_rename_autosave + if autosave == false then + return + end + for uri, _ in pairs(edited_uris) do + local bufnr = vim.uri_to_bufnr(uri) + local was_open = buf_was_modified[bufnr] ~= nil + local was_modified = buf_was_modified[bufnr] + local should_save = autosave == true or (autosave == "unmodified" and not was_modified) + -- Autosave changed buffers if they were not modified before + if should_save then + vim.api.nvim_buf_call(bufnr, function() + vim.cmd.update({ mods = { emsg_silent = true, noautocmd = true } }) + end) + + -- Delete buffers that weren't open before + if not was_open then + vim.api.nvim_buf_delete(bufnr, { force = true }) + end + end + end + end +end + +return M diff --git a/lua/oil/lsp/workspace.lua b/lua/oil/lsp/workspace.lua new file mode 100644 index 0000000..2e67f69 --- /dev/null +++ b/lua/oil/lsp/workspace.lua @@ -0,0 +1,304 @@ +local fs = require("oil.fs") +local ms = require("vim.lsp.protocol").Methods +if vim.fn.has("nvim-0.10") == 0 then + ms = { + workspace_willCreateFiles = "workspace/willCreateFiles", + workspace_didCreateFiles = "workspace/didCreateFiles", + workspace_willDeleteFiles = "workspace/willDeleteFiles", + workspace_didDeleteFiles = "workspace/didDeleteFiles", + workspace_willRenameFiles = "workspace/willRenameFiles", + workspace_didRenameFiles = "workspace/didRenameFiles", + } +end + +local M = {} + +---@param method string +---@return lsp.Client[] +local function get_clients(method) + if vim.fn.has("nvim-0.10") == 1 then + return vim.lsp.get_clients({ method = method }) + else + local clients = vim.lsp.get_active_clients() + return vim.tbl_filter(function(client) + return client.supports_method(method) + end, clients) + end +end + +---@param glob string|vim.lpeg.Pattern +---@param path string +---@return boolean +local function match_glob(glob, path) + -- nvim-0.10 will have vim.glob.to_lpeg, so this will be a LPeg pattern + if type(glob) ~= "string" then + return glob:match(path) ~= nil + end + + -- older versions fall back to glob2regpat + local pat = vim.fn.glob2regpat(glob) + local ignorecase = vim.o.ignorecase + vim.o.ignorecase = false + local ok, match = pcall(vim.fn.match, path, pat) + vim.o.ignorecase = ignorecase + if not ok then + error(match) + end + return match >= 0 +end + +---@param client lsp.Client +---@param filters nil|lsp.FileOperationFilter[] +---@param paths string[] +---@return nil|string[] +local function get_matching_paths(client, filters, paths) + if not filters then + return nil + end + + local match_fns = {} + for _, filter in ipairs(filters) do + if filter.scheme == nil or filter.scheme == "file" then + local pattern = filter.pattern + local glob = pattern.glob + local ignore_case = pattern.options and pattern.options.ignoreCase + if ignore_case then + glob = glob:lower() + end + ---@type nil|vim.lpeg.Pattern + local glob_pattern = vim.glob and vim.glob.to_lpeg and vim.glob.to_lpeg(glob) + local matches = pattern.matches + table.insert(match_fns, function(path) + local is_dir = vim.fn.isdirectory(path) == 1 + if matches and ((matches == "file" and is_dir) or (matches == "folder" and not is_dir)) then + return false + end + + if ignore_case then + path = path:lower() + end + return match_glob(glob_pattern or glob, path) + end) + end + end + local function match_any_pattern(workspace, path) + local relative_path = path:sub(workspace:len() + 2) + for _, match_fn in ipairs(match_fns) do + if match_fn(relative_path) then + return true + end + -- nvim 0.9 doesn't have full glob support, so we need to check the full path as well as the + -- relative path because `**/*.ts` won't match `foo.ts` + if vim.fn.has("nvim-0.10") == 0 and match_fn(path) then + return true + end + end + return false + end + + local workspace_folders = vim.tbl_map(function(folder) + return vim.uri_to_fname(folder.uri) + end, client.workspace_folders) + local function get_matching_workspace(path) + for _, workspace in ipairs(workspace_folders) do + if fs.is_subpath(workspace, path) then + return workspace + end + end + end + + local ret = {} + for _, path in ipairs(paths) do + local workspace = get_matching_workspace(path) + if workspace and match_any_pattern(workspace, path) then + table.insert(ret, path) + end + end + if vim.tbl_isempty(ret) then + return nil + else + return ret + end +end + +---@param method string The method to call +---@param capability_name string The name of the fileOperations server capability +---@param files string[] The files and folders that will be created +---@param options table|nil +---@return nil|{edit: lsp.WorkspaceEdit, offset_encoding: string}[] +---@return nil|string|lsp.ResponseError err +local function will_file_operation(method, capability_name, files, options) + options = options or {} + local clients = get_clients(method) + + local edits = {} + for _, client in ipairs(clients) do + local filters = vim.tbl_get( + client.server_capabilities, + "workspace", + "fileOperations", + capability_name, + "filters" + ) + local matching_files = get_matching_paths(client, filters, files) + if matching_files then + local params = { + files = vim.tbl_map(function(file) + return { + uri = vim.uri_from_fname(file), + } + end, matching_files), + } + ---@diagnostic disable-next-line: invisible + local result, err = client.request_sync(method, params, options.timeout_ms or 1000, 0) + if result and result.result then + if options.apply_edits ~= false then + vim.lsp.util.apply_workspace_edit(result.result, client.offset_encoding) + end + table.insert(edits, { edit = result.result, offset_encoding = client.offset_encoding }) + else + return nil, err or result and result.err + end + end + end + return edits +end + +---@param method string The method to call +---@param capability_name string The name of the fileOperations server capability +---@param files string[] The files and folders that will be created +local function did_file_operation(method, capability_name, files) + local clients = get_clients(method) + for _, client in ipairs(clients) do + local filters = vim.tbl_get( + client.server_capabilities, + "workspace", + "fileOperations", + capability_name, + "filters" + ) + local matching_files = get_matching_paths(client, filters, files) + if matching_files then + local params = { + files = vim.tbl_map(function(file) + return { + uri = vim.uri_from_fname(file), + } + end, matching_files), + } + ---@diagnostic disable-next-line: invisible + client.notify(method, params) + end + end +end + +--- Notify the server that the client is about to create files. +---@param files string[] The files and folders that will be created +---@param options table|nil Optional table which holds the following optional fields: +--- - timeout_ms (integer|nil, default 1000): +--- Time in milliseconds to block for rename requests. +--- - apply_edits (boolean|nil, default true): +--- Apply any workspace edits from these file operations. +---@return nil|{edit: lsp.WorkspaceEdit, offset_encoding: string}[] +---@return nil|string|lsp.ResponseError err +function M.will_create_files(files, options) + return will_file_operation(ms.workspace_willCreateFiles, "willCreate", files, options) +end + +--- Notify the server that files were created from within the client. +---@param files string[] The files and folders that will be created +function M.did_create_files(files) + did_file_operation(ms.workspace_didCreateFiles, "didCreate", files) +end + +--- Notify the server that the client is about to delete files. +---@param files string[] The files and folders that will be deleted +---@param options table|nil Optional table which holds the following optional fields: +--- - timeout_ms (integer|nil, default 1000): +--- Time in milliseconds to block for rename requests. +--- - apply_edits (boolean|nil, default true): +--- Apply any workspace edits from these file operations. +---@return nil|{edit: lsp.WorkspaceEdit, offset_encoding: string}[] +---@return nil|string|lsp.ResponseError err +function M.will_delete_files(files, options) + return will_file_operation(ms.workspace_willDeleteFiles, "willDelete", files, options) +end + +--- Notify the server that files were deleted from within the client. +---@param files string[] The files and folders that were deleted +function M.did_delete_files(files) + did_file_operation(ms.workspace_didDeleteFiles, "didDelete", files) +end + +--- Notify the server that the client is about to rename files. +---@param files table Mapping of old_path -> new_path +---@param options table|nil Optional table which holds the following optional fields: +--- - timeout_ms (integer|nil, default 1000): +--- Time in milliseconds to block for rename requests. +--- - apply_edits (boolean|nil, default true): +--- Apply any workspace edits from these file operations. +---@return nil|{edit: lsp.WorkspaceEdit, offset_encoding: string}[] +---@return nil|string|lsp.ResponseError err +function M.will_rename_files(files, options) + options = options or {} + local clients = get_clients(ms.workspace_willRenameFiles) + + local edits = {} + for _, client in ipairs(clients) do + local filters = vim.tbl_get( + client.server_capabilities, + "workspace", + "fileOperations", + "willRename", + "filters" + ) + local matching_files = get_matching_paths(client, filters, vim.tbl_keys(files)) + if matching_files then + local params = { + files = vim.tbl_map(function(src_file) + return { + oldUri = vim.uri_from_fname(src_file), + newUri = vim.uri_from_fname(files[src_file]), + } + end, matching_files), + } + local result, err = + ---@diagnostic disable-next-line: invisible + client.request_sync(ms.workspace_willRenameFiles, params, options.timeout_ms or 1000, 0) + if result and result.result then + if options.apply_edits ~= false then + vim.lsp.util.apply_workspace_edit(result.result, client.offset_encoding) + end + table.insert(edits, { edit = result.result, offset_encoding = client.offset_encoding }) + else + return nil, err or result and result.err + end + end + end + return edits +end + +--- Notify the server that files were renamed from within the client. +---@param files table Mapping of old_path -> new_path +function M.did_rename_files(files) + local clients = get_clients(ms.workspace_didRenameFiles) + for _, client in ipairs(clients) do + local filters = + vim.tbl_get(client.server_capabilities, "workspace", "fileOperations", "didRename", "filters") + local matching_files = get_matching_paths(client, filters, vim.tbl_keys(files)) + if matching_files then + local params = { + files = vim.tbl_map(function(src_file) + return { + oldUri = vim.uri_from_fname(src_file), + newUri = vim.uri_from_fname(files[src_file]), + } + end, matching_files), + } + ---@diagnostic disable-next-line: invisible + client.notify(ms.workspace_didRenameFiles, params) + end + end +end + +return M diff --git a/lua/oil/lsp_helpers.lua b/lua/oil/lsp_helpers.lua deleted file mode 100644 index b78a257..0000000 --- a/lua/oil/lsp_helpers.lua +++ /dev/null @@ -1,188 +0,0 @@ -local config = require("oil.config") -local fs = require("oil.fs") -local util = require("oil.util") - -local M = {} - ----@param filepath string ----@param pattern lsp.FileOperationPattern ----@return boolean -local function file_matches(filepath, pattern) - local is_dir = vim.fn.isdirectory(filepath) == 1 - if pattern.matches then - if (pattern.matches == "file" and is_dir) or (pattern.matches == "folder" and not is_dir) then - return false - end - end - - if vim.lsp._watchfiles then - local glob = pattern.glob - local path = filepath - if vim.tbl_get(pattern, "options", "ignoreCase") then - glob, path = glob:lower(), path:lower() - end - return vim.lsp._watchfiles._match(glob, path) - end - - local pat = vim.fn.glob2regpat(pattern.glob) - if vim.tbl_get(pattern, "options", "ignoreCase") then - pat = "\\c" .. pat - end - - local ignorecase = vim.o.ignorecase - vim.o.ignorecase = false - local match = vim.fn.match(filepath, pat) >= 0 - vim.o.ignorecase = ignorecase - return match -end - ----@param filepath string ----@param filters lsp.FileOperationFilter[] ----@return boolean -local function any_match(filepath, filters) - for _, filter in ipairs(filters) do - local scheme_match = not filter.scheme or filter.scheme == "file" - if scheme_match and file_matches(filepath, filter.pattern) then - return true - end - end - return false -end - ----@return nil|{src: string, dest: string} -local function get_matching_paths(client, path_pairs) - local filters = - vim.tbl_get(client.server_capabilities, "workspace", "fileOperations", "willRename", "filters") - if not filters then - return nil - end - local ret = {} - for _, pair in ipairs(path_pairs) do - if fs.is_subpath(client.config.root_dir, pair.src) then - local relative_file = pair.src:sub(client.config.root_dir:len() + 2) - if any_match(pair.src, filters) or any_match(relative_file, filters) then - table.insert(ret, pair) - end - end - end - if vim.tbl_isempty(ret) then - return nil - else - return ret - end -end - ----@return table -local function get_buffers_status() - local buffers_status = {} - local buffers = vim.api.nvim_list_bufs() - for _, bufnr in ipairs(buffers) do - buffers_status[bufnr] = { - is_modified = vim.bo[bufnr].modified, - } - end - return buffers_status -end - ----Process LSP rename in the background ----@param actions oil.MoveAction[] -M.will_rename_files = function(actions) - local path_pairs = {} - for _, action in ipairs(actions) do - local _, src_path = util.parse_url(action.src_url) - assert(src_path) - local src_file = fs.posix_to_os_path(src_path) - local _, dest_path = util.parse_url(action.dest_url) - assert(dest_path) - local dest_file = fs.posix_to_os_path(dest_path) - table.insert(path_pairs, { src = src_file, dest = dest_file }) - end - - local clients = vim.lsp.get_active_clients() - for _, client in ipairs(clients) do - local matching_paths = get_matching_paths(client, path_pairs) - if matching_paths then - client.request("workspace/willRenameFiles", { - files = vim.tbl_map(function(pair) - return { - oldUri = vim.uri_from_fname(pair.src), - newUri = vim.uri_from_fname(pair.dest), - } - end, matching_paths), - }, function(_, result) - if result then - local buffers_status = get_buffers_status() - vim.lsp.util.apply_workspace_edit(result, client.offset_encoding) - local autosave = config.lsp_rename_autosave - if autosave == false then - return - end - - for uri in pairs(result.changes) do - local bufnr = vim.uri_to_bufnr(uri) - local is_open = buffers_status[bufnr] ~= nil - local is_modified = is_open and buffers_status[bufnr].is_modified - local should_save = autosave == true or (autosave == "unmodified" and not is_modified) - if should_save then - vim.api.nvim_buf_call(bufnr, function() - vim.cmd.update() - end) - if not is_open then - vim.api.nvim_buf_delete(bufnr, { force = true }) - end - end - end - end - end) - end - end -end - --- LSP types from core Neovim - ----A filter to describe in which file operation requests or notifications ----the server is interested in receiving. ---- ----@since 3.16.0 ----@class lsp.FileOperationFilter ----A Uri scheme like `file` or `untitled`. ----@field scheme? string ----The actual file operation pattern. ----@field pattern lsp.FileOperationPattern - ----A pattern to describe in which file operation requests or notifications ----the server is interested in receiving. ---- ----@since 3.16.0 ----@class lsp.FileOperationPattern ----The glob pattern to match. Glob patterns can have the following syntax: ----- `*` to match one or more characters in a path segment ----- `?` to match on one character in a path segment ----- `**` to match any number of path segments, including none ----- `{}` to group sub patterns into an OR expression. (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) ----- `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) ----- `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) ----@field glob string ----Whether to match files or folders with this pattern. ---- ----Matches both if undefined. ----@field matches? lsp.FileOperationPatternKind ----Additional options used during matching. ----@field options? lsp.FileOperationPatternOptions - ----A pattern kind describing if a glob pattern matches a file a folder or ----both. ---- ----@since 3.16.0 ----@alias lsp.FileOperationPatternKind ----| "file" # file ----| "folder" # folder - ----Matching options for the file operation pattern. ---- ----@since 3.16.0 ----@class lsp.FileOperationPatternOptions ----The pattern should be matched ignoring casing. ----@field ignoreCase? boolean - -return M diff --git a/lua/oil/mutator/init.lua b/lua/oil/mutator/init.lua index 60a6ba6..4ab659d 100644 --- a/lua/oil/mutator/init.lua +++ b/lua/oil/mutator/init.lua @@ -4,7 +4,7 @@ local cache = require("oil.cache") local columns = require("oil.columns") local config = require("oil.config") local constants = require("oil.constants") -local lsp_helpers = require("oil.lsp_helpers") +local lsp_helpers = require("oil.lsp.helpers") local oil = require("oil") local parser = require("oil.mutator.parser") local preview = require("oil.mutator.preview") @@ -367,18 +367,7 @@ end ---@param actions oil.Action[] ---@param cb fun(err: nil|string) M.process_actions = function(actions, cb) - -- send all renames to LSP servers - local moves = {} - for _, action in ipairs(actions) do - if action.type == "move" then - local src_adapter = assert(config.get_adapter_by_scheme(action.src_url)) - local dest_adapter = assert(config.get_adapter_by_scheme(action.dest_url)) - if src_adapter.name == "files" and dest_adapter.name == "files" then - table.insert(moves, action) - end - end - end - lsp_helpers.will_rename_files(moves) + local did_complete = lsp_helpers.will_perform_file_operations(actions) -- Convert some cross-adapter moves to a copy + delete for _, action in ipairs(actions) do @@ -426,6 +415,7 @@ M.process_actions = function(actions, cb) return end if idx > #actions then + did_complete() finish() return end