refactor: rename oil to canola across entire codebase
Problem: the codebase still used the upstream \`oil\` naming everywhere — URL schemes, the \`:Oil\` command, highlight groups, user events, module paths, filetypes, buffer/window variables, LuaCATS type annotations, vimdoc help tags, syntax groups, and internal identifiers. Solution: mechanical rename of every reference. URL schemes now use \`canola://\` (plus \`canola-ssh://\`, \`canola-s3://\`, \`canola-sss://\`, \`canola-trash://\`, \`canola-test://\`). The \`:Canola\` command replaces \`:Oil\`. All highlight groups, user events, augroups, namespaces, filetypes, require paths, type annotations, help tags, and identifiers follow suit. The \`upstream\` remote to \`stevearc/oil.nvim\` has been removed and the \`vim.g.oil\` deprecation shim dropped.
This commit is contained in:
parent
67ad0632a6
commit
e6b42168c9
70 changed files with 1571 additions and 1555 deletions
121
lua/canola/lsp/helpers.lua
Normal file
121
lua/canola/lsp/helpers.lua
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
local config = require('canola.config')
|
||||
local fs = require('canola.fs')
|
||||
local util = require('canola.util')
|
||||
local workspace = require('canola.lsp.workspace')
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param actions canola.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))
|
||||
src_path = fs.posix_to_os_path(src_path)
|
||||
dest_path = fs.posix_to_os_path(assert(dest_path))
|
||||
if src_adapter.name == 'files' and dest_adapter.name == 'files' then
|
||||
moves[src_path] = dest_path
|
||||
elseif src_adapter.name == 'files' then
|
||||
table.insert(deletes, src_path)
|
||||
elseif dest_adapter.name == 'files' then
|
||||
table.insert(creates, src_path)
|
||||
end
|
||||
elseif action.type == 'create' then
|
||||
local scheme, path = util.parse_url(action.url)
|
||||
path = fs.posix_to_os_path(assert(path))
|
||||
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)
|
||||
path = fs.posix_to_os_path(assert(path))
|
||||
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)
|
||||
path = fs.posix_to_os_path(assert(path))
|
||||
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
|
||||
local timeout_ms = config.lsp_file_methods.timeout_ms
|
||||
accum(workspace.will_create_files(creates, { timeout_ms = timeout_ms }))
|
||||
accum(workspace.will_delete_files(deletes, { timeout_ms = timeout_ms }))
|
||||
accum(workspace.will_rename_files(moves, { timeout_ms = timeout_ms }))
|
||||
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_file_methods.autosave_changes
|
||||
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
|
||||
351
lua/canola/lsp/workspace.lua
Normal file
351
lua/canola/lsp/workspace.lua
Normal file
|
|
@ -0,0 +1,351 @@
|
|||
local fs = require('canola.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 vim.lsp.Client[]
|
||||
local function get_clients(method)
|
||||
if vim.fn.has('nvim-0.10') == 1 then
|
||||
return vim.lsp.get_clients({ method = method })
|
||||
else
|
||||
---@diagnostic disable-next-line: deprecated
|
||||
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 vim.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
|
||||
|
||||
-- Some language servers use forward slashes as path separators on Windows (LuaLS)
|
||||
-- We no longer need this after 0.12: https://github.com/neovim/neovim/commit/322a6d305d088420b23071c227af07b7c1beb41a
|
||||
if vim.fn.has('nvim-0.12') == 0 and fs.is_windows then
|
||||
glob = glob:gsub('/', '\\')
|
||||
end
|
||||
|
||||
---@type string|vim.lpeg.Pattern
|
||||
local glob_to_match = glob
|
||||
if vim.glob and vim.glob.to_lpeg then
|
||||
glob = glob:gsub('{(.-)}', function(s)
|
||||
local patterns = vim.split(s, ',')
|
||||
local filtered = {}
|
||||
for _, pat in ipairs(patterns) do
|
||||
if pat ~= '' then
|
||||
table.insert(filtered, pat)
|
||||
end
|
||||
end
|
||||
if #filtered == 0 then
|
||||
return ''
|
||||
end
|
||||
-- HACK around https://github.com/neovim/neovim/issues/28931
|
||||
-- find alternations and sort them by length to try to match the longest first
|
||||
if vim.fn.has('nvim-0.11') == 0 then
|
||||
table.sort(filtered, function(a, b)
|
||||
return a:len() > b:len()
|
||||
end)
|
||||
end
|
||||
return '{' .. table.concat(filtered, ',') .. '}'
|
||||
end)
|
||||
|
||||
glob_to_match = vim.glob.to_lpeg(glob)
|
||||
end
|
||||
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_to_match, 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
|
||||
-- The glob pattern might be relative to workspace OR absolute
|
||||
if match_fn(relative_path) or 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 or {})
|
||||
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),
|
||||
}
|
||||
local result, err
|
||||
if vim.fn.has('nvim-0.11') == 1 then
|
||||
result, err = client:request_sync(method, params, options.timeout_ms or 1000, 0)
|
||||
else
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
result, err = client.request_sync(method, params, options.timeout_ms or 1000, 0)
|
||||
end
|
||||
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),
|
||||
}
|
||||
if vim.fn.has('nvim-0.11') == 1 then
|
||||
client:notify(method, params)
|
||||
else
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
client.notify(method, params)
|
||||
end
|
||||
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
|
||||
if vim.fn.has('nvim-0.11') == 1 then
|
||||
result, err =
|
||||
client:request_sync(ms.workspace_willRenameFiles, params, options.timeout_ms or 1000, 0)
|
||||
else
|
||||
result, err =
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
client.request_sync(ms.workspace_willRenameFiles, params, options.timeout_ms or 1000, 0)
|
||||
end
|
||||
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),
|
||||
}
|
||||
if vim.fn.has('nvim-0.11') == 1 then
|
||||
client:notify(ms.workspace_didRenameFiles, params)
|
||||
else
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
client.notify(ms.workspace_didRenameFiles, params)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
Loading…
Add table
Add a link
Reference in a new issue