Skip to content

Commit 9d22023

Browse files
committed
feat: add module for LSP based file operation utilities
1 parent 292b2c9 commit 9d22023

File tree

4 files changed

+240
-0
lines changed

4 files changed

+240
-0
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,20 @@ local opts = {
110110
},
111111
},
112112
},
113+
-- Configuration of LSP file operation functionality
114+
file_operations = {
115+
-- the timeout when executing LSP client operations
116+
timeout = 10000,
117+
-- fully disable/enable file operation methods
118+
operations = {
119+
willRename = true,
120+
didRename = true,
121+
willCreate = true,
122+
didCreate = true,
123+
willDelete = true,
124+
didDelete = true,
125+
},
126+
},
113127
-- A custom flags table to be passed to all language servers (`:h lspconfig-setup`)
114128
flags = {
115129
exit_timeout = 5000,

lua/astrolsp/config.lua

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,18 @@
4343
---@field format_on_save boolean|AstroLSPFormatOnSaveOpts? control formatting on save options
4444
---@field disabled true|string[]? true to disable all or a list like table of language server names to disable formatting
4545

46+
---@class AstroLSPFileOperationsOperationsOpts
47+
---@field willCreate boolean? enable/disable pre-create file notifications
48+
---@field willDelete boolean? enable/disable pre-create file notifications
49+
---@field willRename boolean? enable/disable pre-rename file notifications
50+
---@field didCreate boolean? enable/disable post-create file notifications
51+
---@field didDelete boolean? enable/disable post-create file notifications
52+
---@field didRename boolean? enable/disable post-rename file notifications
53+
54+
---@class AstroLSPFileOperationsOpts
55+
---@field timeout integer? timeout length in ms when executing LSP client file operations
56+
---@field operations AstroLSPFileOperationsOperationsOpts? enable/disable file operations
57+
4658
---@class AstroLSPOpts
4759
---Configuration of auto commands
4860
---The key into the table is the group name for the auto commands (`:h augroup`) and the value
@@ -131,6 +143,25 @@
131143
---}
132144
---```
133145
---@field config lspconfig.options?
146+
---Configure LSP based file operations
147+
---Example:
148+
--
149+
---```lua
150+
---file_operations = {
151+
--- -- the timeout when executing LSP client operations
152+
--- timeout = 10000,
153+
--- -- fully disable/enable file operation methods
154+
--- operations = {
155+
--- willRenameFiles = true,
156+
--- didRenameFiles = true,
157+
--- willCreateFiles = true,
158+
--- didCreateFiles = true,
159+
--- willDeleteFiles = true,
160+
--- didDeleteFiles = true,
161+
--- }
162+
--- }
163+
---```
164+
---@field file_operations AstroLSPFileOperationsOpts?
134165
---A custom flags table to be passed to all language servers (`:h lspconfig-setup`)
135166
---Example:
136167
--
@@ -268,6 +299,7 @@ local M = {
268299
capabilities = {},
269300
---@diagnostic disable-next-line: missing-fields
270301
config = {},
302+
file_operations = { timeout = 10000, operations = {} },
271303
flags = {},
272304
formatting = { format_on_save = { enabled = true }, disabled = {} },
273305
handlers = {},

lua/astrolsp/file_operations.lua

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
---AstroNvim LSP File Operation Utilities
2+
---
3+
---Utilities for working with LSP based file operations
4+
---
5+
---This module is heavily inspired by nvim-lsp-file-operations
6+
---https://github.com/antosha417/nvim-lsp-file-operations/tree/master
7+
---
8+
---This module can be loaded with `local astrolsp_fileops = require "astrolsp.file_operations"`
9+
---
10+
---copyright 2025
11+
---license GNU General Public License v3.0
12+
---@class astrolsp.file_operations
13+
local M = {}
14+
15+
local config = vim.tbl_get(require "astrolsp", "config", "file_operations") or {}
16+
17+
-- TODO: remove check when dropping support for Neovim v0.9
18+
local get_clients = vim.lsp.get_clients or vim.lsp.get_active_clients
19+
20+
---@class AstroLSPFileOperationsRename
21+
---@field from string the original filename
22+
---@field to string the new filename
23+
24+
local filter_cache = {}
25+
local match_filters = function(filters, name)
26+
local fname = vim.fn.fnamemodify(name, ":p")
27+
for _, filter in pairs(filters) do
28+
if not filter_cache[filter] then filter_cache = {} end
29+
if filter_cache[filter][fname] == nil then
30+
local matched = false
31+
local pattern = filter.pattern
32+
local match_type = pattern.matches
33+
local is_dir = string.sub(fname, #fname) == "/"
34+
if not match_type or (match_type == "folder" and is_dir) or (match_type == "file" and not is_dir) then
35+
local regex = vim.fn.glob2regpat(pattern.glob)
36+
if vim.tbl_get(pattern, "options", "ignorecase") then regex = "\\c" .. regex end
37+
local previous_ignorecase = vim.o.ignorecase
38+
vim.o.ignorecase = false
39+
matched = vim.fn.match(fname, regex) ~= -1
40+
vim.o.ignorecase = previous_ignorecase
41+
end
42+
filter_cache[filter][fname] = matched
43+
end
44+
if filter_cache[filter][fname] then return true end
45+
end
46+
return false
47+
end
48+
49+
--- Notify LSP clients that file(s) were created
50+
---@param fnames string|string[] a file or list of files that were created
51+
function M.didCreateFiles(fnames)
52+
if not vim.tbl_get(config, "operations", "didCreate") then return end
53+
for _, client in pairs(get_clients()) do
54+
local did_create = vim.tbl_get(client, "server_capabilities", "workspace", "fileOperations", "didCreate")
55+
if did_create then
56+
if type(fnames) == "string" then fnames = { fnames } end
57+
local filters = did_create.filters or {}
58+
local filtered = vim.tbl_filter(function(fname) return match_filters(filters, fname) end, fnames)
59+
if next(filtered) then
60+
client.notify(
61+
"workspace/didCreateFiles",
62+
{ files = vim.tbl_map(function(fname) return { uri = vim.uri_from_fname(fname) } end, filtered) }
63+
)
64+
end
65+
end
66+
end
67+
end
68+
69+
--- Notify LSP clients that file(s) were deleted
70+
---@param fnames string|string[] a file or list of files that were deleted
71+
function M.didDeleteFiles(fnames)
72+
if not vim.tbl_get(config, "operations", "didDelete") then return end
73+
for _, client in pairs(get_clients()) do
74+
local did_delete = vim.tbl_get(client, "server_capabilities", "workspace", "fileOperations", "didDelete")
75+
if did_delete ~= nil then
76+
if type(fnames) == "string" then fnames = { fnames } end
77+
local filters = did_delete.filters or {}
78+
local filtered = vim.tbl_filter(function(fname) return match_filters(filters, fname) end, fnames)
79+
if next(filtered) then
80+
client.notify(
81+
"workspace/didDeleteFiles",
82+
{ files = vim.tbl_map(function(fname) return { uri = vim.uri_from_fname(fname) } end, filtered) }
83+
)
84+
end
85+
end
86+
end
87+
end
88+
89+
--- Notify LSP clients that file(s) were renamed
90+
---@param renames AstroLSPFileOperationsRename|AstroLSPFileOperationsRename[] a table or list of tables of files that were renamed
91+
function M.didRenameFiles(renames)
92+
if not vim.tbl_get(config, "operations", "didRename") then return end
93+
for _, client in pairs(get_clients()) do
94+
local did_rename = vim.tbl_get(client, "server_capabilities", "workspace", "fileOperations", "didRename")
95+
if did_rename ~= nil then
96+
if renames.from then renames = { renames } end
97+
local filters = did_rename.filters or {}
98+
local filtered = vim.tbl_filter(
99+
function(rename) return rename.from and rename.to and match_filters(filters, rename.from) end,
100+
renames
101+
)
102+
if next(filtered) then
103+
client.notify("workspace/didRenameFiles", {
104+
files = vim.tbl_map(
105+
function(rename) return { oldUri = vim.uri_from_fname(rename.from), newUri = vim.uri_from_fname(rename.to) } end,
106+
filtered
107+
),
108+
})
109+
end
110+
end
111+
end
112+
end
113+
114+
local getWorkspaceEdit = function(client, req, params)
115+
local success, resp = pcall(client.request_sync, req, params, config.timeout)
116+
if success then return resp.result end
117+
end
118+
119+
--- Notify LSP clients that file(s) are going to be created
120+
---@param fnames string|string[] a file or list of files that will be created
121+
function M.willCreateFiles(fnames)
122+
if not vim.tbl_get(config, "operations", "willCreate") then return end
123+
for _, client in pairs(get_clients()) do
124+
local will_create = vim.tbl_get(client, "server_capabilities", "workspace", "fileOperations", "willCreate")
125+
if will_create then
126+
if type(fnames) == "string" then fnames = { fnames } end
127+
local filters = will_create.filters or {}
128+
local filtered = vim.tbl_filter(function(fname) return match_filters(filters, fname) end, fnames)
129+
if next(filtered) then
130+
local edit = getWorkspaceEdit(
131+
client,
132+
"workspace/didCreateFiles",
133+
{ files = vim.tbl_map(function(fname) return { uri = vim.uri_from_fname(fname) } end, filtered) }
134+
)
135+
if edit then vim.lsp.util.apply_workspace_edit(edit, client.offset_encoding) end
136+
end
137+
end
138+
end
139+
end
140+
141+
--- Notify LSP clients that file(s) are going to be deleted
142+
---@param fnames string|string[] a file or list of files that will be deleted
143+
function M.willDeleteFiles(fnames)
144+
if not vim.tbl_get(config, "operations", "willDelete") then return end
145+
for _, client in pairs(get_clients()) do
146+
local will_delete = vim.tbl_get(client, "server_capabilities", "workspace", "fileOperations", "willDelete")
147+
if will_delete then
148+
if type(fnames) == "string" then fnames = { fnames } end
149+
local filters = will_delete.filters or {}
150+
local filtered = vim.tbl_filter(function(fname) return match_filters(filters, fname) end, fnames)
151+
if next(filtered) then
152+
local edit = getWorkspaceEdit(
153+
client,
154+
"workspace/willDeleteFiles",
155+
{ files = vim.tbl_map(function(fname) return { uri = vim.uri_from_fname(fname) } end, filtered) }
156+
)
157+
if edit then vim.lsp.util.apply_workspace_edit(edit, client.offset_encoding) end
158+
end
159+
end
160+
end
161+
end
162+
163+
--- Notify LSP clients that file(s) are going to be renamed
164+
---@param renames AstroLSPFileOperationsRename|AstroLSPFileOperationsRename[] a table or list of tables of files that will be renamed
165+
function M.willRenameFiles(renames)
166+
if not vim.tbl_get(config, "operations", "willRename") then return end
167+
for _, client in pairs(get_clients()) do
168+
local will_rename = vim.tbl_get(client, "server_capabilities", "workspace", "fileOperations", "willRename")
169+
if will_rename then
170+
if renames.from then renames = { renames } end
171+
local filters = will_rename.filters or {}
172+
local filtered = vim.tbl_filter(
173+
function(rename) return rename.from and rename.to and match_filters(filters, rename.from) end,
174+
renames
175+
)
176+
if next(filtered) then
177+
local edit = getWorkspaceEdit(client, "workspace/willRenameFiles", {
178+
files = vim.tbl_map(
179+
function(rename) return { oldUri = vim.uri_from_fname(rename.from), newUri = vim.uri_from_fname(rename.to) } end,
180+
filtered
181+
),
182+
})
183+
if edit then vim.lsp.util.apply_workspace_edit(edit, client.offset_encoding) end
184+
end
185+
end
186+
end
187+
end
188+
189+
return M

lua/astrolsp/init.lua

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,11 @@ end
208208
function M.setup(opts)
209209
M.config = vim.tbl_deep_extend("force", M.config, opts)
210210

211+
-- enable necessary capabilities for enabled LSP file operations
212+
M.config.capabilities = vim.tbl_deep_extend("force", M.config.capabilities or {}, {
213+
workspace = { fileOperations = vim.tbl_get(M.config, "file_operations", "operations") },
214+
})
215+
211216
-- normalize format_on_save to table format
212217
if vim.tbl_get(M.config, "formatting", "format_on_save") == false then
213218
M.config.formatting.format_on_save = { enabled = false }

0 commit comments

Comments
 (0)