feat: add linter config and clean up codebase

This commit is contained in:
2024-06-12 09:05:53 +02:00
parent 186e95d6b5
commit b404648928
7 changed files with 580 additions and 313 deletions
+6 -11
View File
@@ -26,11 +26,11 @@ end
local reload_server_config = utils.debounce_with_id(function(name, events) local reload_server_config = utils.debounce_with_id(function(name, events)
utils.info(("Reloading server with new config"):format(name), name) utils.info(("Reloading server with new config"):format(name), name)
---@type Server? ---@type Server|nil
local server = servers[name] local server = servers[name]
if server and server.config.enable then if server and server.config.enable then
server:unload() server:deinit()
servers[name] = nil servers[name] = nil
end end
@@ -54,11 +54,11 @@ local reload_server_config = utils.debounce_with_id(function(name, events)
if #server:get_ft_buffers() ~= 0 then if #server:get_ft_buffers() ~= 0 then
server:setup() server:setup()
else else
server:register() server:init()
end end
servers[name] = server servers[name] = server
end, 100) end, 1000)
local function process_change(error, filename, events) local function process_change(error, filename, events)
if error then if error then
@@ -103,12 +103,7 @@ local function load_configs()
::continue:: ::continue::
end end
vim.uv.fs_event_start( vim.uv.fs_event_start(vim.uv.new_fs_event(), CONFIG_DIR, {}, vim.schedule_wrap(process_change))
vim.uv.new_fs_event(),
CONFIG_DIR,
{},
vim.schedule_wrap(process_change)
)
end end
--- Setup diagnostics UI --- Setup diagnostics UI
@@ -148,7 +143,7 @@ function M.setup()
for _, server in pairs(servers) do for _, server in pairs(servers) do
if server.config.enable then if server.config.enable then
server:register() server:init()
end end
end end
end end
+17 -17
View File
@@ -21,22 +21,22 @@ M.new = {}
--- Load LSP keybinds --- Load LSP keybinds
---@param server Server ---@param server Server
function M:load(server, bufnr) function M:init(server, bufnr)
self.old[bufnr] = {} self.old[bufnr] = {}
for _, mode in ipairs(MODE_TYPES) do for _, mode in ipairs(MODE_TYPES) do
vim.tbl_extend("error", self.old[bufnr], vim.api.nvim_buf_get_keymap(bufnr, mode)) vim.tbl_extend("error", self.old[bufnr], vim.api.nvim_buf_get_keymap(bufnr, mode))
end end
self.new[bufnr] = { self.new[bufnr] = {
{ mode = { "n" }, lhs = "<leader>df", rhs = vim.diagnostic.open_float }, { mode = { "n" }, lhs = "<leader>df", rhs = vim.diagnostic.open_float },
{ mode = { "n" }, lhs = "[d", rhs = vim.diagnostic.goto_prev }, { mode = { "n" }, lhs = "[d", rhs = vim.diagnostic.goto_prev },
{ mode = { "n" }, lhs = "]d", rhs = vim.diagnostic.goto_next }, { mode = { "n" }, lhs = "]d", rhs = vim.diagnostic.goto_next },
{ mode = { "n" }, lhs = "gD", rhs = vim.lsp.buf.declaration }, { mode = { "n" }, lhs = "gD", rhs = vim.lsp.buf.declaration },
{ mode = { "n", "i" }, lhs = "<C-k>", rhs = vim.lsp.buf.hover }, { mode = { "n", "i" }, lhs = "<C-k>", rhs = vim.lsp.buf.hover },
{ mode = { "n", "i" }, lhs = "<C-j>", rhs = vim.lsp.buf.signature_help }, { mode = { "n", "i" }, lhs = "<C-j>", rhs = vim.lsp.buf.signature_help },
{ mode = { "n", "i" }, lhs = "<C-h>", rhs = vim.lsp.buf.document_highlight }, { mode = { "n", "i" }, lhs = "<C-h>", rhs = vim.lsp.buf.document_highlight },
{ mode = { "n" }, lhs = "<leader>lr", rhs = server.ca_rename }, { mode = { "n" }, lhs = "<leader>lr", rhs = server.ca_rename },
{ mode = { "n" }, lhs = "<leader>la", rhs = vim.lsp.buf.code_action }, { mode = { "n" }, lhs = "<leader>la", rhs = vim.lsp.buf.code_action },
{ mode = { "n", "x" }, lhs = "<leader>lf", rhs = vim.lsp.buf.format }, { mode = { "n", "x" }, lhs = "<leader>lf", rhs = vim.lsp.buf.format },
{ {
mode = { "n", "i" }, mode = { "n", "i" },
@@ -57,17 +57,17 @@ function M:load(server, bufnr)
vim.list_extend(self.new[bufnr], { vim.list_extend(self.new[bufnr], {
{ mode = "n", lhs = "<leader>dl", rhs = telescope.diagnostics }, { mode = "n", lhs = "<leader>dl", rhs = telescope.diagnostics },
{ mode = "n", lhs = "<leader>lD", rhs = telescope.lsp_type_definitions }, { mode = "n", lhs = "<leader>lD", rhs = telescope.lsp_type_definitions },
{ mode = "n", lhs = "gd", rhs = telescope.lsp_definitions }, { mode = "n", lhs = "gd", rhs = telescope.lsp_definitions },
{ mode = "n", lhs = "gi", rhs = telescope.lsp_implementations }, { mode = "n", lhs = "gi", rhs = telescope.lsp_implementations },
{ mode = "n", lhs = "gr", rhs = telescope.lsp_references }, { mode = "n", lhs = "gr", rhs = telescope.lsp_references },
}) })
else else
vim.list_extend(self.new[bufnr], { vim.list_extend(self.new[bufnr], {
{ mode = "n", lhs = "<leader>dl", rhs = vim.diagnostic.setloclist }, { mode = "n", lhs = "<leader>dl", rhs = vim.diagnostic.setloclist },
{ mode = "n", lhs = "<leader>ld", rhs = vim.lsp.buf.type_definition }, { mode = "n", lhs = "<leader>ld", rhs = vim.lsp.buf.type_definition },
{ mode = "n", lhs = "gd", rhs = vim.lsp.buf.definition }, { mode = "n", lhs = "gd", rhs = vim.lsp.buf.definition },
{ mode = "n", lhs = "gi", rhs = vim.lsp.buf.implementation }, { mode = "n", lhs = "gi", rhs = vim.lsp.buf.implementation },
{ mode = "n", lhs = "gr", rhs = vim.lsp.buf.references }, { mode = "n", lhs = "gr", rhs = vim.lsp.buf.references },
}) })
end end
@@ -81,7 +81,7 @@ function M:load(server, bufnr)
end end
end end
function M:unload(bufnr) function M:deinit(bufnr)
if self.new[bufnr] then if self.new[bufnr] then
for _, keymap in ipairs(self.new[bufnr]) do for _, keymap in ipairs(self.new[bufnr]) do
-- pcall to avoid error if keymap was already removed, -- pcall to avoid error if keymap was already removed,
+163
View File
@@ -0,0 +1,163 @@
local utils = require("utils")
---@class Linter
---@field name string
---@field namespace number
---@field augroup number
---@field buffers number[]
---@field config LinterConfig
M = {}
M.__index = M
---@alias Group
---| "lnum"
---| "col"
---| "severity"
---| "message"
---@class LinterConfig
---@field cmd string[]
---@field stdin? boolean
---@field stdout? boolean
---@field stderr? boolean
---@field pattern string
---@field groups Group[]
---@field severity_map table<string, vim.diagnostic.Severity>
---@field source? string
---@field debounce? number
M.config = {}
function M.validate(name, config)
local ok, resp = pcall(vim.validate, {
name = { name, "string" },
config = { config, "table" },
})
if ok then
ok, resp = pcall(vim.validate, {
cmd = {
config.cmd,
function(t)
return utils.is_list(t, "string")
end,
"list of strings",
},
stdin = { config.stdin, "boolean", true },
stdout = { config.stdout, "boolean", true },
stderr = { config.stderr, "boolean", true },
pattern = { config.pattern, "string" },
groups = {
config.groups,
function(t)
return utils.is_list(t, "string")
end,
"list of strings",
},
severity_map = {
config.severity_map,
function(t)
return utils.is_map(t, "string", "number")
end,
"map of string and number",
},
debounce = { config.debounce, "number", true },
source = { config.source, "string", true },
})
end
if not ok then
utils.err(("Invalid config for linter:\n%s"):format(resp))
return false
end
return true
end
function M:run(bufnr)
local input
-- TODO: add placeholder variables for when not using stdin
if self.config.stdin then
input = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
end
vim.system(
self.config.cmd,
{ stdin = input },
vim.schedule_wrap(function(out)
local output
if self.config.stdout then
output = out.stdout or ""
end
if self.config.stderr then
output = out.stderr or ""
end
local output_lines = vim.fn.split(output, "\n", false)
local diagnostics = {}
for _, line in ipairs(output_lines) do
local ok, resp = pcall(
vim.diagnostic.match,
line,
self.config.pattern,
self.config.groups,
self.config.severity_map
)
if not ok then
utils.err(tostring(resp))
return
elseif not resp then
utils.err(("Failed to parse linter output:\n%s"):format(line))
end
resp.source = self.config.source
table.insert(diagnostics, resp)
end
vim.diagnostic.set(self.namespace, bufnr, diagnostics)
end)
)
end
function M:init(bufnr)
table.insert(self.buffers, bufnr)
vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, {
buffer = bufnr,
callback = utils.debounce(function()
self:run(bufnr)
end, self.config.debounce),
group = self.augroup,
})
self:run(bufnr)
end
function M:deinit()
for _, bufnr in ipairs(self.buffers) do
vim.api.nvim_buf_clear_namespace(bufnr, self.namespace, 0, -1)
end
self.buffers = {}
vim.api.nvim_clear_autocmds({ group = self.augroup })
end
--- Create a new instance
---@param name string
---@param config LinterConfig
---@return Linter|nil
function M.new(name, config)
if not M.validate(name, config) then
return
end
config.debounce = config.debounce or 100
local linter = {
name = name,
namespace = vim.api.nvim_create_namespace(name),
augroup = vim.api.nvim_create_augroup(name, {}),
buffers = {},
config = config,
}
return setmetatable(linter, M)
end
return M
+118 -125
View File
@@ -1,100 +1,96 @@
local utils = require("utils") local utils = require("utils")
---@class PostInstallStep ---@class PostInstallStep
---@field command string ---@field cmd string[]
---@field args string[]
---@class MasonPackage ---@class MasonPackage
---@field dependencies MasonPackage[]? ---@field dependencies? MasonPackage[]
---@field config MasonPackageConfig ---@field config MasonPackageConfig
local M = {} local M = {}
M.__index = M M.__index = M
---@class MasonPackageConfig ---@class MasonPackageConfig
---@field name string? ---@field [1]? string
---@field version string? ---@field name? string
---@field dependencies MasonPackageConfig[]? ---@field version? string
---@field post_install PostInstallStep[]? ---@field dependencies? string[]|MasonPackageConfig[]
---@field post_install? PostInstallStep[]
M.config = {} M.config = {}
--- Validate MasonPackageConfig --- Validate MasonPackageConfig
---@param config MasonPackageConfig ---@param config string|MasonPackageConfig
---@return boolean ---@return boolean
function M.validate(config) function M.validate(config)
local ok, resp = pcall(vim.validate, { config = { config, { "table" } } }) local ok, resp = pcall(vim.validate, { config = { config, { "table" } } })
if not ok then if ok then
goto check_resp ok, resp = pcall(vim.validate, {
end name = { config.name or config[1], { "string" }, true },
ok, resp = pcall(
vim.validate, {
name = { config.name, { "string" }, true },
version = { config.version, { "string" }, true }, version = { config.version, { "string" }, true },
dependencies = { dependencies = {
config.dependencies, function(f) config.dependencies,
if not f then function(f)
return true if not f then
end return true
end
if not utils.is_list(f) then if utils.is_list(f, "string") then
return false return true
end elseif not utils.is_list(f) then
for _, dep in ipairs(f) do
if not M.validate(dep) then
return false return false
end end
end
return true for _, dep in ipairs(f) do
end, if not M.validate(dep) then
return false
end
end
return true
end,
"list of dependencies",
}, },
post_install = { post_install = {
config.post_install, function(field) config.post_install,
if not field then function(field)
return true if not field then
end return true
if not utils.is_list(field) then
return false
end
for _, step in ipairs(field) do
local o, r = pcall(vim.validate, { step = { step, { "table" } } })
if not ok then
goto check_r
end end
o, r = pcall( if not utils.is_list(field) then
vim.validate, {
command = { step.command, { "string" } },
args = {
step.args, function(f)
return utils.is_list_or_nil(f, "string")
end, "list of strings or nil",
},
}
)
::check_r::
if not o then
utils.err(("Invalid config for post_install step: %s"):format(r))
return false return false
end end
for _, step in ipairs(field) do
local o, r = pcall(vim.validate, { step = { step, { "table" } } })
if o then
o, r = pcall(vim.validate, {
cmd = {
step.cmd,
function(f)
return utils.is_list(f, "string")
end,
"list of strings or nil",
},
})
end
if not o then
utils.err(("Invalid config for post_install step: %s"):format(r))
return false
end
return true
end
return true return true
end end,
"list of steps",
return true
end,
}, },
} })
) end
::check_resp::
if not ok then if not ok then
utils.err(("Invalid config for %s:\n%s"):format(config.name, resp)) utils.err(("Invalid config for %s:\n%s"):format(config.name or config[1] or config, resp))
return false return false
end end
@@ -107,52 +103,47 @@ function M:run_post_install(pkg)
---@param step PostInstallStep ---@param step PostInstallStep
---@param msg string ---@param msg string
local function log_err(step, msg) local function log_err(step, msg)
local cmd = step.command local cmd = table.concat(step.cmd, " ")
if step.args then
cmd = cmd .. " " .. table.concat(step.args, " ")
end
utils.err( utils.err(
("Post installation step for %s:\n`%s`\nfailed with:\n%s"):format( ("Post installation step for %s:\n`%s`\nfailed with:\n%s"):format(
self.config.name, cmd, msg self.config.name,
), "lsp.package:run_post_install" cmd,
msg
),
"lsp.package:run_post_install"
) )
end end
if self.config.post_install then if self.config.post_install then
utils.info("running post install")
for _, step in ipairs(self.config.post_install) do for _, step in ipairs(self.config.post_install) do
utils.info("running post install step")
local cwd = pkg:get_install_path() local cwd = pkg:get_install_path()
local command = step.command local args = step.cmd
local prog = table.remove(args, 1)
if command:find("[/\\]") then if prog:find("[/\\]") then
command = vim.fn.resolve(("%s/%s"):format(cwd, command)) prog = vim.fn.resolve(("%s/%s"):format(cwd, prog))
end end
if not utils.is_executable(command) then if not utils.is_executable(prog) then
log_err(step, "command not executable") log_err(step, "command not executable")
return return
end end
local job = require("plenary.job"):new( local job = require("plenary.job"):new({
{ command = prog,
command = step.command, args = args,
args = step.args, cwd = pkg:get_install_path(),
cwd = pkg:get_install_path(), enabled_recording = true,
enabled_recording = true, on_exit = vim.schedule_wrap(function(job, code, signal)
on_exit = function(job, code, signal) if code ~= 0 or signal ~= 0 then
if code ~= 0 or signal ~= 0 then log_err(step, table.concat(job:stderr_result(), "\n"))
local cmd = command return
if step.args then end
cmd = cmd .. " " .. table.concat(step.args, " ") utils.info("post install step done")
end end),
})
log_err(step, table.concat(job:stderr_result(), "\n"))
end
end,
}
)
job:start() job:start()
end end
end end
@@ -186,42 +177,40 @@ function M:mason_install(on_done)
local err local err
handle:on( handle:on(
"stderr", vim.schedule_wrap( "stderr",
function(msg) vim.schedule_wrap(function(msg)
err = (err or "") .. msg err = (err or "") .. msg
end end)
)
) )
handle:once( handle:once(
"closed", vim.schedule_wrap( "closed",
function() vim.schedule_wrap(function()
local is_installed = pkg:is_installed() local is_installed = pkg:is_installed()
if is_installed then if is_installed then
utils.info( utils.info(
("Successfully installed %s"):format(self.config.name), ("Successfully installed %s"):format(self.config.name),
"lsp.package:mason_install" "lsp.package:mason_install"
) )
self:run_post_install(pkg) self:run_post_install(pkg)
else
if err then
err = ":\n" .. err
else else
if err then err = ""
err = ":\n" .. err
else
err = ""
end
utils.err(
("Failed to install %s%s"):format(self.config.name, err),
"lsp.package:mason_install"
)
end end
if on_done then utils.err(
on_done(is_installed) ("Failed to install %s%s"):format(self.config.name, err),
end "lsp.package:mason_install"
)
end end
)
if on_done then
on_done(is_installed)
end
end)
) )
end end
@@ -276,15 +265,19 @@ function M:install(on_done)
end end
--- Create a new instance --- Create a new instance
---@param config MasonPackageConfig? ---@param config MasonPackageConfig|string
---@return MasonPackage? ---@return MasonPackage|nil
function M.new(config) function M.new(config)
config = config or {} if type(config) == "string" then
config = { config }
end
if not M.validate(config) then if not M.validate(config) then
return return
end end
config.name = config.name or config[1]
local pkg = { config = config } local pkg = { config = config }
if pkg.config.dependencies then if pkg.config.dependencies then
+149 -91
View File
@@ -1,33 +1,44 @@
local utils = require("utils")
local keymap = require("lsp.keymap") local keymap = require("lsp.keymap")
local utils = require("utils")
---@class Linter
local Linter = require("lsp.linter")
---@class MasonPackage ---@class MasonPackage
local MasonPackage = require("lsp.package") local MasonPackage = require("lsp.package")
-- override type, seems to be incorrect in either lspconfig or vim.lsp -- override type, seems to be incorrect in either lspconfig or vim.lsp
---@class lspconfig.Config ---@class lspconfig.Config
---@field root_dir function ---@field root_dir function
---@class Server ---@class Server
---@field name string? ---@field name? string
---@field mason MasonPackage? ---@field mason? MasonPackage
---@field client vim.lsp.Client? ---@field client? vim.lsp.Client
---@field attached_buffers number[]? ---@field attached_buffers? number[]
---@field manager lspconfig.Manager ---@field manager lspconfig.Manager
---@field linters? Linter[]
---@field config ServerConfig ---@field config ServerConfig
local M = {} local M = {}
M.__index = M M.__index = M
---@class ServerConfig ---@class ServerConfig
---@field enable boolean? ---@field enable? boolean
---@field dependencies string[]? ---@field dependencies? string[]
---@field mason MasonPackageConfig? ---@field mason? string|MasonPackageConfig
---@field root_patterns string[]? ---@field root_patterns? string[]
---@field keymaps Keymap[]? ---@field keymaps? Keymap[]
---@field lspconfig lspconfig.Config? ---@field linters? LinterConfig[]
---@field lspconfig? lspconfig.Config
M.config = {} M.config = {}
--- Validate ServerConfig --- Validate ServerConfig
---@param config ServerConfig ---@param config ServerConfig
---@return boolean ---@return boolean
function M.validate(name, config) function M.validate(name, config)
local ok, resp = pcall(vim.validate, { config = { config, { "table" } } }) local ok, resp = pcall(vim.validate, { config = { config, { "table" } } })
if ok then if ok then
ok, resp = pcall(vim.validate, { ok, resp = pcall(vim.validate, {
enable = { config.enable, { "boolean" }, true }, enable = { config.enable, { "boolean" }, true },
@@ -35,57 +46,69 @@ function M.validate(name, config)
config.dependencies, config.dependencies,
function(f) function(f)
return utils.is_list_or_nil(f, "string") return utils.is_list_or_nil(f, "string")
end, "list of strings or nil", end,
}, "list of strings or nil",
mason = {
config.mason, function(f)
if f == nil then return true end
return MasonPackage.validate(f)
end,
}, },
mason = { config.mason, { "string", "table" }, true },
root_patterns = { root_patterns = {
config.root_patterns, config.root_patterns,
function(f) function(f)
return utils.is_list_or_nil(f, "string") return utils.is_list_or_nil(f, "string")
end, "list of strings or nil", end,
"list of strings or nil",
}, },
keymaps = { keymaps = {
config.keymaps, function(f) config.keymaps,
if not f then return true end function(f)
if not utils.is_list(f, "table") then if not f then
return false return true
end end
for _, key in ipairs(f) do
local o, r = pcall(vim.validate, { if not utils.is_list(f, "table") then
mode = { key.mode, { "s", "t" } },
lhs = { key.lhs, "s" },
rhs = { key.rhs, { "s", "f" } },
opts = { key.opts, "t", true },
})
if not o then
utils.err(("Invalid keymap:\n%s"):format(r))
return false return false
end end
end
return true for _, key in ipairs(f) do
end, "list of keymaps", local o, r = pcall(vim.validate, {
mode = { key.mode, { "s", "t" } },
lhs = { key.lhs, "s" },
rhs = { key.rhs, { "s", "f" } },
opts = { key.opts, "t", true },
})
if not o then
utils.err(("Invalid keymap:\n%s"):format(r))
return false
end
end
return true
end,
"list of keymaps",
}, },
lspconfig = { config.lspconfig, { "table" }, true }, lspconfig = { config.lspconfig, { "table" }, true },
}) })
end end
if not ok then if not ok then
utils.err(("Invalid config for %s:\n%s"):format(name, resp)) utils.err(("Invalid config for %s:\n%s"):format(name, resp))
return false return false
end end
return true return true
end end
--- Rename Code Action --- Rename Code Action
function M.ca_rename() function M.ca_rename()
local ts_utils = utils.try_require("nvim-treesitter.ts_utils") local ts_utils = utils.try_require("nvim-treesitter.ts_utils")
if not ts_utils then return end if not ts_utils then
return
end
local identifier_types = { local identifier_types = {
"IDENTIFIER", "identifier", "variable_name", "word", "IDENTIFIER",
"identifier",
"variable_name",
"word",
} }
local node = ts_utils.get_node_at_cursor() local node = ts_utils.get_node_at_cursor()
if not node or not vim.list_contains(identifier_types, node:type()) then if not node or not vim.list_contains(identifier_types, node:type()) then
@@ -98,8 +121,7 @@ function M.ca_rename()
local buf = vim.api.nvim_create_buf(false, true) local buf = vim.api.nvim_create_buf(false, true)
local min_width = 10 local min_width = 10
local max_width = 50 local max_width = 50
local default_width = math.min(max_width, math.max(min_width, local default_width = math.min(max_width, math.max(min_width, vim.str_utfindex(old) + 1))
vim.str_utfindex(old) + 1))
local row, col, _, _ = node:range() local row, col, _, _ = node:range()
local win = vim.api.nvim_open_win(buf, true, { local win = vim.api.nvim_open_win(buf, true, {
relative = "win", relative = "win",
@@ -116,22 +138,20 @@ function M.ca_rename()
}) })
vim.api.nvim_buf_set_lines(buf, 0, -1, false, { old }) vim.api.nvim_buf_set_lines(buf, 0, -1, false, { old })
vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI", "TextChangedP" }, vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI", "TextChangedP" }, {
{ buffer = buf,
buffer = buf, callback = function()
callback = function() local win_width = vim.api.nvim_win_get_width(win)
local win_width = vim.api.nvim_win_get_width(win) local content = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
local content = vim.api.nvim_buf_get_lines(buf, 0, -1, false) if #content > 0 then
if #content > 0 then local cwidth = vim.str_utfindex(content[1] or "") + 1
local cwidth = vim.str_utfindex(content[1] or "") + 1 local new_width = math.min(max_width, math.max(min_width, cwidth))
local new_width = math.min(max_width, if new_width ~= win_width then
math.max(min_width, cwidth)) vim.api.nvim_win_set_width(win, new_width)
if new_width ~= win_width then
vim.api.nvim_win_set_width(win, new_width)
end
end end
end, end
}) end,
})
vim.keymap.set({ "n", "i", "x" }, "<cr>", function() vim.keymap.set({ "n", "i", "x" }, "<cr>", function()
local content = vim.api.nvim_buf_get_lines(buf, 0, -1, false) local content = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
@@ -146,15 +166,14 @@ function M.ca_rename()
vim.api.nvim_win_close(win, true) vim.api.nvim_win_close(win, true)
vim.cmd.stopinsert() vim.cmd.stopinsert()
end, { buffer = buf }) end, { buffer = buf })
vim.keymap.set({ "n", "x" }, "<esc>", vim.keymap.set({ "n", "x" }, "<esc>", function()
function() vim.api.nvim_win_close(win, true) end, vim.api.nvim_win_close(win, true)
{ buffer = buf }) end, { buffer = buf })
vim.keymap.set({ "n", "x" }, "q", vim.keymap.set({ "n", "x" }, "q", function()
function() vim.api.nvim_win_close(win, true) end, vim.api.nvim_win_close(win, true)
{ buffer = buf }) end, { buffer = buf })
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("^v$<C-g>", true, vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("^v$<C-g>", true, false, true), "n", true)
false, true), "n", true)
end end
--- Called when language server attaches --- Called when language server attaches
@@ -168,7 +187,12 @@ function M:on_attach(client, bufnr)
self.attached_buffers = self.attached_buffers or {} self.attached_buffers = self.attached_buffers or {}
table.insert(self.attached_buffers, bufnr) table.insert(self.attached_buffers, bufnr)
keymap:load(self, bufnr) keymap:init(self, bufnr)
if self.linters then
for _, linter in ipairs(self.linters) do
linter:init(bufnr)
end
end
-- For document highlight -- For document highlight
vim.cmd.highlight({ "link LspReferenceRead Visual", bang = true }) vim.cmd.highlight({ "link LspReferenceRead Visual", bang = true })
@@ -178,9 +202,8 @@ function M:on_attach(client, bufnr)
vim.opt.updatetime = 300 vim.opt.updatetime = 300
require("lsp-inlayhints").on_attach(client, bufnr, false) require("lsp-inlayhints").on_attach(client, bufnr, false)
vim.lsp.handlers["textDocument/hover"] = vim.lsp.with( vim.lsp.handlers["textDocument/hover"] =
vim.lsp.handlers.hover, vim.lsp.with(vim.lsp.handlers.hover, { border = "single" })
{ border = "single" })
vim.lsp.handlers["textDocument/signatureHelp"] = vim.lsp.handlers["textDocument/signatureHelp"] =
vim.lsp.with(vim.lsp.handlers.signature_help, { border = "single" }) vim.lsp.with(vim.lsp.handlers.signature_help, { border = "single" })
@@ -205,8 +228,8 @@ function M:configure_client()
local cmp_nvim_lsp = utils.try_require("cmp_nvim_lsp") local cmp_nvim_lsp = utils.try_require("cmp_nvim_lsp")
if cmp_nvim_lsp then if cmp_nvim_lsp then
capabilities = vim.tbl_deep_extend("force", capabilities, capabilities =
cmp_nvim_lsp.default_capabilities()) vim.tbl_deep_extend("force", capabilities, cmp_nvim_lsp.default_capabilities())
end end
-- local epo = utils.try_require("epo") -- local epo = utils.try_require("epo")
@@ -230,19 +253,31 @@ function M:configure_client()
if not ok then if not ok then
utils.err( utils.err(
("Failed to load on_attach for %s:\n%s"):format(self.name, ret), ("Failed to load on_attach for %s:\n%s"):format(self.name, ret),
"lsp.server:configure_client") "lsp.server:configure_client"
)
end end
end end
local ok, ret = pcall(lspconfig[self.name].setup, self.config.lspconfig) local ok, ret = pcall(lspconfig[self.name].setup, self.config.lspconfig)
if not ok then if not ok then
utils.err(("Failed to setup LSP server %s with lspconfig: %s"):format( utils.err(("Failed to setup LSP server %s with lspconfig: %s"):format(self.name, ret))
self.name, ret))
return return
end end
self.manager = lspconfig[self.name].manager self.manager = lspconfig[self.name].manager
for _, bufnr in ipairs(self:get_ft_buffers()) do for _, bufnr in ipairs(self:get_ft_buffers()) do
self.manager:try_add_wrapper(bufnr) self.manager:try_add_wrapper(bufnr)
end end
if self.config.linters then
self.linters = {}
for i, config in ipairs(self.config.linters) do
local linter = Linter.new(("%s_linter%d"):format(self.name, i), config)
if linter then
table.insert(self.linters, linter)
end
end
end
end end
function M:get_ft_buffers() function M:get_ft_buffers()
@@ -278,8 +313,12 @@ function M:install(on_done)
--- Handle install result --- Handle install result
---@param success boolean ---@param success boolean
local function handle_result(success) local function handle_result(success)
if not success then self.config.enable = false end if not success then
if on_done then on_done(success) end self.config.enable = false
end
if on_done then
on_done(success)
end
end end
self.mason:install(handle_result) self.mason:install(handle_result)
@@ -291,13 +330,18 @@ function M:setup()
if #missing_deps > 0 then if #missing_deps > 0 then
utils.warn( utils.warn(
("Disabling %s because the following package(s) are not installed: %s"):format( ("Disabling %s because the following package(s) are not installed: %s"):format(
self.name, table.concat(missing_deps, ", "))) self.name,
table.concat(missing_deps, ", ")
)
)
self.config.enable = false self.config.enable = false
return return
end end
if self.mason then if self.mason then
self:install(function(success) self:install(function(success)
if success then self:configure_client() end if success then
self:configure_client()
end
end) end)
elseif vim.fn.executable(self.config.lspconfig.cmd[1]) == 1 then elseif vim.fn.executable(self.config.lspconfig.cmd[1]) == 1 then
self:configure_client() self:configure_client()
@@ -307,51 +351,65 @@ function M:setup()
end end
end end
--- Register autocmd for setting up LSP server upon entering a buffer of related filetype --- Load autocmd for setting up LSP server upon entering a buffer of related filetype
function M:register() function M:init()
local group = vim.api.nvim_create_augroup("lsp_bootstrap_" .. self.name, {}) local group = vim.api.nvim_create_augroup("lsp_bootstrap_" .. self.name, {})
vim.api.nvim_create_autocmd("FileType", { vim.api.nvim_create_autocmd("FileType", {
once = true, once = true,
pattern = self.config.lspconfig.filetypes or {}, pattern = self.config.lspconfig.filetypes or {},
callback = function() self:setup() end, callback = function()
self:setup()
end,
group = group, group = group,
}) })
end end
function M:unload() function M:deinit()
if self.attached_buffers then if self.attached_buffers then
for _, bufnr in ipairs(self.attached_buffers) do for _, bufnr in ipairs(self.attached_buffers) do
keymap:unload(bufnr) keymap:deinit(bufnr)
end end
end end
if self.client then if self.client then
self.client.stop() self.client.stop()
self.client = nil self.client = nil
end end
vim.api.nvim_clear_autocmds({ group = "lsp_bootstrap_" .. self.name }) vim.api.nvim_clear_autocmds({ group = "lsp_bootstrap_" .. self.name })
if self.linters then
for _, linter in ipairs(self.linters) do
linter:deinit()
end
self.linters = nil
end
require("lspconfig")[self.name] = nil require("lspconfig")[self.name] = nil
end end
--- Create a new instance --- Create a new instance
---@param name string ---@param name string
---@param config ServerConfig? ---@param config? ServerConfig
---@return Server? ---@return Server|nil
function M.new(name, config) function M.new(name, config)
config = config or {} config = config or {}
if not M.validate(name, config) then return end if not M.validate(name, config) then
local ok, resp = pcall(require, "lspconfig.server_configurations." .. name)
if not ok then
utils.err(("Server with name %s does not exist in lspconfig"):format(
name))
return return
end end
config.lspconfig = vim.tbl_deep_extend("keep", config.lspconfig or {}, local ok, resp = pcall(require, "lspconfig.server_configurations." .. name)
resp.default_config) if not ok then
utils.err(("Server with name %s does not exist in lspconfig"):format(name))
return
end
config.lspconfig = vim.tbl_deep_extend("keep", config.lspconfig or {}, resp.default_config)
local server = { name = name, config = config } local server = { name = name, config = config }
if server.config.mason then if server.config.mason then
local pkg = MasonPackage.new(server.config.mason) local pkg = MasonPackage.new(server.config.mason)
if pkg then server.mason = pkg end if pkg then
server.mason = pkg
end
end end
return setmetatable(server, M) return setmetatable(server, M)
end end
+117 -69
View File
@@ -4,7 +4,7 @@ M.os_name = vim.uv.os_uname().sysname
--- Get the module path of a file --- Get the module path of a file
---@param file string ---@param file string
---@return string? ---@return string|nil
local function get_module_path(file) local function get_module_path(file)
for _, rtp in ipairs(vim.api.nvim_list_runtime_paths()) do for _, rtp in ipairs(vim.api.nvim_list_runtime_paths()) do
if file:sub(1, #rtp) == rtp then if file:sub(1, #rtp) == rtp then
@@ -21,13 +21,13 @@ end
--- Send a notification --- Send a notification
---@param msg string Message to send ---@param msg string Message to send
---@param title string? Title of notification ---@param title? string Title of notification
---@param level integer Log level ---@param level integer Log level
local function notify(msg, title, level) local function notify(msg, title, level)
if not title then if not title then
local info = debug.getinfo(3) local info = debug.getinfo(3)
local file = info.source local file = info.source
and (info.source:sub(1, 1) == "@" and info.source:sub(2) or info.source) and (info.source:sub(1, 1) == "@" and info.source:sub(2) or info.source)
or nil or nil
local module = file and (get_module_path(file) or file) or nil local module = file and (get_module_path(file) or file) or nil
title = module and module .. (info.name and info.name ~= "" and ":" .. info.name or "") title = module and module .. (info.name and info.name ~= "" and ":" .. info.name or "")
@@ -97,28 +97,28 @@ end
--- Send a debug notification --- Send a debug notification
---@param msg string Message to send ---@param msg string Message to send
---@param title string? Title of notification ---@param title? string Title of notification
function M.debug(msg, title) function M.debug(msg, title)
notify(msg, title, vim.log.levels.DEBUG) notify(msg, title, vim.log.levels.DEBUG)
end end
--- Send an info notification --- Send an info notification
---@param msg string Message to send ---@param msg string Message to send
---@param title string? Title of notification ---@param title? string Title of notification
function M.info(msg, title) function M.info(msg, title)
notify(msg, title, vim.log.levels.INFO) notify(msg, title, vim.log.levels.INFO)
end end
--- Send a warning notification --- Send a warning notification
---@param msg string Message to send ---@param msg string Message to send
---@param title string? Title of notification ---@param title? string Title of notification
function M.warn(msg, title) function M.warn(msg, title)
notify(msg, title, vim.log.levels.WARN) notify(msg, title, vim.log.levels.WARN)
end end
--- Send an error notification --- Send an error notification
---@param msg string Message to send ---@param msg string Message to send
---@param title string? Title of notification ---@param title? string Title of notification
function M.err(msg, title) function M.err(msg, title)
notify(msg, title, vim.log.levels.ERROR) notify(msg, title, vim.log.levels.ERROR)
end end
@@ -136,21 +136,29 @@ function M.try_require(module)
M.err(("Failed to load module %s:\n%s"):format(module, resp)) M.err(("Failed to load module %s:\n%s"):format(module, resp))
end end
--- Checks if it is possible to require a module
---@param module string
---@return boolean
function M.has_module(module)
local has_module, _ = pcall(require, module)
return has_module
end
---@class FormatOptions ---@class FormatOptions
---@field cmd string[] Command to run. The following keywords get replaces by the specified values: ---@field cmd string[] Command to run. The following keywords get replaces by the specified values:
--- * %file% - path to the current file --- * %file% - path to the current file
--- * %filename% - name of the current file
--- * %row_start% - first row of selection --- * %row_start% - first row of selection
--- * %row_end% - last row of selection --- * %row_end% - last row of selection
--- * %col_start% - first column position of selection --- * %col_start% - first column position of selection
--- * %col_end% - last column position of selection --- * %col_end% - last column position of selection
--- * %byte_start% - byte count of first cell in selection --- * %byte_start% - byte count of first cell in selection
--- * %byte_end% - byte count of last cell in selection --- * %byte_end% - byte count of last cell in selection
---@field stdin boolean? Pass text to stdin. False by default. ---@field stdin? boolean Pass text to stdin. Assumes in-place formatting on False. False by default.
---@field stdout boolean? Use stdout as the result. False by default. ---@field stdout? boolean Use stdout as the result. False by default.
---@field stderr boolean? Use stderr as the result. False by default. ---@field stderr? boolean Use stderr as the result. False by default.
---@field in_place boolean? The file is formatted in-place by `cmd`. False by default. ---@field auto_indent? boolean Perform auto indent on formatted range. False by default.
---@field auto_indent boolean? Perform auto indent on formatted range. False by default. ---@field only_selection? boolean Only send the selected lines to `stdin`. False by default.
---@field selection boolean? Only format the currently selected lines. False by default.
--- Format buffer --- Format buffer
---@param opts FormatOptions ---@param opts FormatOptions
@@ -160,23 +168,20 @@ function M.format(opts)
stdin = opts.stdin or false, stdin = opts.stdin or false,
stdout = opts.stdout or false, stdout = opts.stdout or false,
stderr = opts.stderr or false, stderr = opts.stderr or false,
in_place = opts.in_place or false,
auto_indent = opts.auto_indent or false, auto_indent = opts.auto_indent or false,
selection = opts.selection or false, only_selection = opts.only_selection or false,
} }
if not opts.in_place and not opts.stdout and not opts.stderr then if opts.stdin and not (opts.stdout or opts.stderr) then
M.err("One of `in_place`, `stdout` or `stderr` must be true.") M.err("`stdin` requires that one of `stdout` or `stderr` is set")
return return
elseif opts.in_place and (opts.selection or opts.stdin or opts.stdout or opts.stderr) then elseif (opts.only_selection or opts.stdout or opts.stderr) and not opts.stdin then
M.err( M.err("`stdout`, `stderr` and `only_selection` requires `stdin` to be set")
"`in_place` is not valid together with any of "
.. "`selection`, `stdin`, `stdout` or `stderr`"
)
return return
end end
local file = vim.fn.expand("%") local file = vim.fn.expand("%")
local filename = vim.fn.expand("%:t")
local mode = vim.fn.mode() local mode = vim.fn.mode()
local is_visual = mode == "v" or mode == "V" or mode == "" local is_visual = mode == "v" or mode == "V" or mode == ""
@@ -199,18 +204,19 @@ function M.format(opts)
end end
local input local input
if opts.selection then if opts.only_selection then
input = vim.api.nvim_buf_get_lines(0, row_start - 1, row_end, false) input = vim.api.nvim_buf_get_lines(0, row_start - 1, row_end, false)
else elseif opts.stdin then
input = vim.api.nvim_buf_get_lines(0, 0, -1, false) input = vim.api.nvim_buf_get_lines(0, 0, -1, false)
end end
if opts.in_place then if not opts.stdin then
vim.api.nvim_buf_call(0, vim.cmd.write) vim.api.nvim_buf_call(0, vim.cmd.write)
end end
for i, arg in ipairs(opts.cmd) do for i, arg in ipairs(opts.cmd) do
arg = arg:gsub("%%file%%", file) arg = arg:gsub("%%file%%", file)
arg = arg:gsub("%%filename%%", filename)
if is_visual then if is_visual then
arg = arg:gsub("%%row_start%%", row_start) arg = arg:gsub("%%row_start%%", row_start)
arg = arg:gsub("%%row_end%%", row_end) arg = arg:gsub("%%row_end%%", row_end)
@@ -222,58 +228,100 @@ function M.format(opts)
opts.cmd[i] = arg opts.cmd[i] = arg
end end
vim.system( local stdout, stderr, err
opts.cmd, local resp = vim.system(opts.cmd, {
{ stdin = opts.stdin and input or nil,
stdin = opts.stdin and input or nil, stdout = opts.stdout and function(e, data)
}, if data then
vim.schedule_wrap(function(out) stdout = stdout and stdout .. data or data
if out.code ~= 0 or out.signal ~= 0 then
local err = out.stderr or ""
M.err(("Failed to format:\n%s"):format(err))
return
end end
if opts.in_place then if e then
vim.api.nvim_buf_call(0, vim.cmd.edit) err = err and err .. e or e
end
end,
stderr = opts.stderr and function(e, data)
if data then
stderr = stderr and stderr .. data or data
end
if e then
err = err and err .. e or e
end
end,
}):wait()
if err then
M.err("Error during formatting:\n%s" .. err)
return
end
if resp.code ~= 0 or resp.signal ~= 0 then
M.err(("Failed to format:\n%s"):format(stderr or ""))
return
end
if not opts.stdin then
vim.api.nvim_buf_call(0, vim.cmd.edit)
else
local output
if opts.stdout then
output = stdout or ""
end
if opts.stderr then
output = stderr or ""
end
output = output:gsub("\n$", "")
local output_lines = vim.fn.split(output, "\n", true)
if opts.only_selection then
vim.api.nvim_buf_set_lines(0, row_start - 1, row_end, false, output_lines)
else
vim.api.nvim_buf_set_lines(0, 0, -1, false, output_lines)
end
if opts.auto_indent then
if is_visual then
vim.api.nvim_command(
("%d,%dnormal! =="):format(row_start, row_start + #output_lines)
)
else else
local output vim.api.nvim_command("normal! gg=G")
if opts.stdout then
output = out.stdout or ""
end
if opts.stderr then
output = out.stderr or ""
end
output = output:gsub("\n$", "")
local output_lines = vim.fn.split(output, "\n", true)
if opts.selection then
vim.api.nvim_buf_set_lines(0, row_start - 1, row_end, false, output_lines)
else
vim.api.nvim_buf_set_lines(0, 0, -1, false, output_lines)
end
if opts.auto_indent then
if is_visual then
vim.api.nvim_command(
("%d,%dnormal! =="):format(row_start, row_start + #output_lines)
)
else
vim.api.nvim_command("normal! gg=G")
end
end
end end
end) end
) end
end end
--- Check if `val` is a list of type `t` (if given) --- Check if `val` is a list of type `t` (if given)
---@param val any ---@param val any
---@param t type? ---@param kt type
---@param vt type
---@return boolean
function M.is_map(val, kt, vt)
if type(val) ~= "table" then
return false
end
for k, v in pairs(val) do
if type(k) ~= kt then
return false
end
if type(v) ~= vt then
return false
end
end
return true
end
--- Check if `val` is a list of type `t` (if given)
---@param val any
---@param t? type
---@return boolean ---@return boolean
function M.is_list(val, t) function M.is_list(val, t)
if type(val) ~= "table" then if not vim.tbl_islist(val) then
return false return false
end end
@@ -291,8 +339,8 @@ function M.is_list(val, t)
end end
--- Check if `val` is a list of type `t` (if given), or nil --- Check if `val` is a list of type `t` (if given), or nil
---@param val any? ---@param val? any
---@param t type? ---@param t? type
---@return boolean ---@return boolean
function M.is_list_or_nil(val, t) function M.is_list_or_nil(val, t)
if val == nil then if val == nil then
+10
View File
@@ -0,0 +1,10 @@
column_width = 100
line_endings = "Unix"
indent_type = "Spaces"
indent_width = 4
quote_style = "AutoPreferDouble"
call_parentheses = "Always"
collapse_simple_statement = "Never"
[sort_requires]
enabled = true