-
Notifications
You must be signed in to change notification settings - Fork 138
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: support all LSP workspace file operations
- Loading branch information
Showing
5 changed files
with
641 additions
and
201 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Oops, something went wrong.