feat: support all LSP workspace file operations (#264)
This commit is contained in:
parent
48d8ea8f4a
commit
250e0af7a5
5 changed files with 641 additions and 201 deletions
224
lua/oil/lsp/_types.lua
Normal file
224
lua/oil/lsp/_types.lua
Normal file
|
|
@ -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<string, lsp.LSPAny>
|
||||||
|
---@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<lsp.DocumentUri, lsp.TextEdit[]>
|
||||||
|
---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<lsp.ChangeAnnotationIdentifier, lsp.ChangeAnnotation>
|
||||||
|
|
||||||
|
---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
|
||||||
110
lua/oil/lsp/helpers.lua
Normal file
110
lua/oil/lsp/helpers.lua
Normal file
|
|
@ -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
|
||||||
304
lua/oil/lsp/workspace.lua
Normal file
304
lua/oil/lsp/workspace.lua
Normal file
|
|
@ -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<string, string> 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<string, string> 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
|
||||||
|
|
@ -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<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
|
|
||||||
|
|
@ -4,7 +4,7 @@ local cache = require("oil.cache")
|
||||||
local columns = require("oil.columns")
|
local columns = require("oil.columns")
|
||||||
local config = require("oil.config")
|
local config = require("oil.config")
|
||||||
local constants = require("oil.constants")
|
local constants = require("oil.constants")
|
||||||
local lsp_helpers = require("oil.lsp_helpers")
|
local lsp_helpers = require("oil.lsp.helpers")
|
||||||
local oil = require("oil")
|
local oil = require("oil")
|
||||||
local parser = require("oil.mutator.parser")
|
local parser = require("oil.mutator.parser")
|
||||||
local preview = require("oil.mutator.preview")
|
local preview = require("oil.mutator.preview")
|
||||||
|
|
@ -367,18 +367,7 @@ end
|
||||||
---@param actions oil.Action[]
|
---@param actions oil.Action[]
|
||||||
---@param cb fun(err: nil|string)
|
---@param cb fun(err: nil|string)
|
||||||
M.process_actions = function(actions, cb)
|
M.process_actions = function(actions, cb)
|
||||||
-- send all renames to LSP servers
|
local did_complete = lsp_helpers.will_perform_file_operations(actions)
|
||||||
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)
|
|
||||||
|
|
||||||
-- Convert some cross-adapter moves to a copy + delete
|
-- Convert some cross-adapter moves to a copy + delete
|
||||||
for _, action in ipairs(actions) do
|
for _, action in ipairs(actions) do
|
||||||
|
|
@ -426,6 +415,7 @@ M.process_actions = function(actions, cb)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if idx > #actions then
|
if idx > #actions then
|
||||||
|
did_complete()
|
||||||
finish()
|
finish()
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue