* save buffers changed by will_rename_files * prevent closing already open buffers * chore: move to config option * chore: fixes * fix: a crash and some formatting --------- Co-authored-by: Steven Arcangeli <506791+stevearc@users.noreply.github.com>
188 lines
6 KiB
Lua
188 lines
6 KiB
Lua
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<number, {is_open: boolean, is_modified: boolean}>
|
||
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
|