|
@@ -1,104 +1,192 @@
|
|
|
local Job = require("plenary.job")
|
|
local Job = require("plenary.job")
|
|
|
local Path = require("plenary.path")
|
|
local Path = require("plenary.path")
|
|
|
-local namespace_name = "maj-peg"
|
|
|
|
|
-local status = {
|
|
|
|
|
- setup = false,
|
|
|
|
|
|
|
+
|
|
|
|
|
+local M = {}
|
|
|
|
|
+local namespace = vim.api.nvim_create_namespace("maj-peg")
|
|
|
|
|
+local group = vim.api.nvim_create_augroup("maj-peg", { clear = true })
|
|
|
|
|
+
|
|
|
|
|
+local state = {
|
|
|
enabled = true,
|
|
enabled = true,
|
|
|
has_mypy = false,
|
|
has_mypy = false,
|
|
|
- has_mypy_baseline = false
|
|
|
|
|
|
|
+ has_baseline = false,
|
|
|
|
|
+ flags = {},
|
|
|
|
|
+ baseline_flags = {}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+local severity_map = {
|
|
|
|
|
+ error = vim.diagnostic.severity.ERROR,
|
|
|
|
|
+ warn = vim.diagnostic.severity.WARN,
|
|
|
|
|
+ warning = vim.diagnostic.severity.WARN,
|
|
|
|
|
+ info = vim.diagnostic.severity.INFO,
|
|
|
|
|
+ note = vim.diagnostic.severity.INFO,
|
|
|
|
|
+ hint = vim.diagnostic.severity.HINT
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-local setup = function(mypy_flags, mypybaseline_flags)
|
|
|
|
|
- local namespace = vim.api.nvim_create_namespace(namespace_name)
|
|
|
|
|
- local group = vim.api.nvim_create_augroup(namespace_name, { clear = true })
|
|
|
|
|
- status.has_mypy = vim.fn.system({ "which", "mypy" }) ~= ""
|
|
|
|
|
- status.has_mypy_baseline = vim.fn.system({ "which", "mypy-baseline" }) ~= ""
|
|
|
|
|
- if mypy_flags == nil then
|
|
|
|
|
- mypy_flags = { "--follow-imports", "silent" }
|
|
|
|
|
|
|
+local function get_project_root(bufname)
|
|
|
|
|
+ if state.has_baseline then
|
|
|
|
|
+ local baseline = vim.fs.find({ ".mypy-baseline" },
|
|
|
|
|
+ { path = bufname, upward = true })[1]
|
|
|
|
|
+ if baseline then
|
|
|
|
|
+ return vim.fs.dirname(baseline)
|
|
|
|
|
+ end
|
|
|
end
|
|
end
|
|
|
- if mypybaseline_flags == nil then
|
|
|
|
|
- mypybaseline_flags = { "filter" }
|
|
|
|
|
|
|
+
|
|
|
|
|
+ local markers = { "pyproject.toml", "setup.cfg", ".git" }
|
|
|
|
|
+ local root = vim.fs.find(markers, { path = bufname, upward = true })[1]
|
|
|
|
|
+ return root and vim.fs.dirname(root) or vim.loop.cwd()
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+local function parse_diagnostic(line)
|
|
|
|
|
+ local sline, scol, eline, ecol, rest = line:match(
|
|
|
|
|
+ "^:(%d+):(%d+):(%d+):(%d+):%s*(.*)$")
|
|
|
|
|
+
|
|
|
|
|
+ if not sline then
|
|
|
|
|
+ sline, rest = line:match("^:(%d+):%s*(.*)$")
|
|
|
|
|
+ if not sline then return nil end
|
|
|
|
|
+ scol, eline, ecol = "1", sline, "1"
|
|
|
end
|
|
end
|
|
|
- vim.api.nvim_create_autocmd({ "FileType" },
|
|
|
|
|
- {
|
|
|
|
|
- pattern = "python",
|
|
|
|
|
- group = group,
|
|
|
|
|
- callback = function(outer_args)
|
|
|
|
|
- vim.api.nvim_create_autocmd({ "BufWritePost", "FileType" }, {
|
|
|
|
|
- buffer = outer_args.buf,
|
|
|
|
|
- group = group,
|
|
|
|
|
- callback = function(args)
|
|
|
|
|
- vim.diagnostic.reset(namespace, args.buf)
|
|
|
|
|
- if status.enabled == false then return end
|
|
|
|
|
- local command = "mypy " ..
|
|
|
|
|
- table.concat(mypy_flags, " ") .. " " ..
|
|
|
|
|
- vim.api.nvim_buf_get_name(args.buf)
|
|
|
|
|
- if status.has_mypy_baseline then
|
|
|
|
|
- command = command ..
|
|
|
|
|
- " | mypy-baseline " ..
|
|
|
|
|
- table.concat(mypybaseline_flags, " ")
|
|
|
|
|
|
|
+
|
|
|
|
|
+ local lnum = tonumber(sline) - 1
|
|
|
|
|
+ local col = tonumber(scol) - 1
|
|
|
|
|
+ local end_lnum = tonumber(eline) - 1
|
|
|
|
|
+ local end_col = tonumber(ecol)
|
|
|
|
|
+
|
|
|
|
|
+ local severity_txt = rest:match("^(%a+):%s+")
|
|
|
|
|
+ local message = rest:gsub("^%a+:%s+", "", 1)
|
|
|
|
|
+
|
|
|
|
|
+ if end_lnum > (lnum + 1) then
|
|
|
|
|
+ col = 0
|
|
|
|
|
+ end_lnum = lnum
|
|
|
|
|
+ end_col = 1
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ lnum = lnum,
|
|
|
|
|
+ col = math.max(col, 0),
|
|
|
|
|
+ end_lnum = end_lnum,
|
|
|
|
|
+ end_col = math.max(end_col, 0),
|
|
|
|
|
+ severity = severity_map[severity_txt] or vim.diagnostic.severity.HINT,
|
|
|
|
|
+ message = message,
|
|
|
|
|
+ source = "maj-peg"
|
|
|
|
|
+ }
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+local function sanitize_for_baseline(line)
|
|
|
|
|
+ return line:gsub(":(%d+):(%d+):%d+:%d+:", ":%1:%2:")
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+local function run_mypy(bufnr)
|
|
|
|
|
+ if not state.enabled or vim.bo[bufnr].filetype ~= "python" then return end
|
|
|
|
|
+
|
|
|
|
|
+ local abs_path = vim.api.nvim_buf_get_name(bufnr)
|
|
|
|
|
+ if abs_path == "" then return end
|
|
|
|
|
+
|
|
|
|
|
+ local root = get_project_root(abs_path)
|
|
|
|
|
+ local rel_path = Path:new(abs_path):make_relative(root)
|
|
|
|
|
+
|
|
|
|
|
+ vim.diagnostic.reset(namespace, bufnr)
|
|
|
|
|
+
|
|
|
|
|
+ local raw_lines = {}
|
|
|
|
|
+ local args = { unpack(state.flags) }
|
|
|
|
|
+ table.insert(args, rel_path)
|
|
|
|
|
+
|
|
|
|
|
+ Job:new({
|
|
|
|
|
+ command = "mypy",
|
|
|
|
|
+ args = args,
|
|
|
|
|
+ cwd = root,
|
|
|
|
|
+ on_stdout = function(_, line)
|
|
|
|
|
+ if line and line:sub(1, #rel_path) == rel_path then
|
|
|
|
|
+ table.insert(raw_lines, line)
|
|
|
|
|
+ end
|
|
|
|
|
+ end,
|
|
|
|
|
+ on_exit = function()
|
|
|
|
|
+ vim.schedule(function()
|
|
|
|
|
+ local final_lines = raw_lines
|
|
|
|
|
+
|
|
|
|
|
+ if state.has_baseline and #raw_lines > 0 then
|
|
|
|
|
+ local clean_map = {}
|
|
|
|
|
+ local clean_input = {}
|
|
|
|
|
+
|
|
|
|
|
+ for i, line in ipairs(raw_lines) do
|
|
|
|
|
+ local clean = sanitize_for_baseline(line)
|
|
|
|
|
+ clean_map[clean] = i
|
|
|
|
|
+ table.insert(clean_input, clean)
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ local baseline_cmd = "mypy-baseline " ..
|
|
|
|
|
+ table.concat(state.baseline_flags, " ")
|
|
|
|
|
+ local filtered_str = vim.fn.system(baseline_cmd,
|
|
|
|
|
+ table.concat(clean_input, "\n"))
|
|
|
|
|
+
|
|
|
|
|
+ local filtered_indices = {}
|
|
|
|
|
+ for line in vim.gsplit(filtered_str, "\n") do
|
|
|
|
|
+ if line ~= "" and clean_map[line] then
|
|
|
|
|
+ filtered_indices[clean_map[line]] = true
|
|
|
|
|
+ end
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ final_lines = {}
|
|
|
|
|
+ for i, line in ipairs(raw_lines) do
|
|
|
|
|
+ if filtered_indices[i] then
|
|
|
|
|
+ table.insert(final_lines, line)
|
|
|
end
|
|
end
|
|
|
- local path = Path:new(vim.api.nvim_buf_get_name(args.buf))
|
|
|
|
|
- :make_relative()
|
|
|
|
|
- local diagnostics = {}
|
|
|
|
|
- Job:new({
|
|
|
|
|
- command = "zsh",
|
|
|
|
|
- args = { "-c", command },
|
|
|
|
|
- on_stdout = function(_, line)
|
|
|
|
|
- if line == nil or line:sub(1, path:len()) ~= path then
|
|
|
|
|
- return
|
|
|
|
|
- end
|
|
|
|
|
-
|
|
|
|
|
- line = line:sub(path:len() + 1)
|
|
|
|
|
- local lnum = line:match(':%d+:')
|
|
|
|
|
- line = line:gsub(':%d: ', '', 1)
|
|
|
|
|
- if lnum == nil then return end
|
|
|
|
|
- lnum = lnum:gsub(':', '')
|
|
|
|
|
- lnum = tonumber(lnum) - 1
|
|
|
|
|
-
|
|
|
|
|
- local severity = vim.diagnostic.severity.INFO
|
|
|
|
|
- local severity_text = line:match('%a+: ')
|
|
|
|
|
- if severity_text == nil or severity_text == "hint: " then
|
|
|
|
|
- severity = vim.diagnostic.severity.HINT
|
|
|
|
|
- elseif severity_text == "error: " then
|
|
|
|
|
- severity = vim.diagnostic.severity.ERROR
|
|
|
|
|
- elseif severity_text == "warn: " then
|
|
|
|
|
- severity = vim.diagnostic.severity.WARN
|
|
|
|
|
- elseif severity_text == "info: " then
|
|
|
|
|
- severity = vim.diagnostic.severity.INFO
|
|
|
|
|
- end
|
|
|
|
|
- line = line:gsub('.+: ', '', 1)
|
|
|
|
|
-
|
|
|
|
|
- table.insert(
|
|
|
|
|
- diagnostics,
|
|
|
|
|
- {
|
|
|
|
|
- lnum = tonumber(lnum),
|
|
|
|
|
- col = 0,
|
|
|
|
|
- source = namespace_name,
|
|
|
|
|
- severity = severity,
|
|
|
|
|
- message = line
|
|
|
|
|
- })
|
|
|
|
|
- end,
|
|
|
|
|
- on_exit = function()
|
|
|
|
|
- vim.schedule(function()
|
|
|
|
|
- vim.diagnostic.set(namespace,
|
|
|
|
|
- args.buf, diagnostics)
|
|
|
|
|
- end)
|
|
|
|
|
- end,
|
|
|
|
|
- }):start()
|
|
|
|
|
end
|
|
end
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ local diagnostics = {}
|
|
|
|
|
+ for _, line in ipairs(final_lines) do
|
|
|
|
|
+ local diag = parse_diagnostic(line:sub(#rel_path + 1))
|
|
|
|
|
+ if diag then
|
|
|
|
|
+ table.insert(diagnostics, diag)
|
|
|
|
|
+ end
|
|
|
|
|
+ end
|
|
|
|
|
+
|
|
|
|
|
+ if vim.api.nvim_buf_is_valid(bufnr) then
|
|
|
|
|
+ vim.diagnostic.set(namespace, bufnr, diagnostics)
|
|
|
|
|
+ end
|
|
|
|
|
+ end)
|
|
|
|
|
+ end,
|
|
|
|
|
+ }):start()
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+M.setup = function(mypy_opts, baseline_opts)
|
|
|
|
|
+ state.has_mypy = vim.fn.executable("mypy") == 1
|
|
|
|
|
+ state.has_baseline = vim.fn.executable("mypy-baseline") == 1
|
|
|
|
|
+
|
|
|
|
|
+ state.flags = mypy_opts or
|
|
|
|
|
+ { "--follow-imports", "silent", "--show-error-end" }
|
|
|
|
|
+ state.baseline_flags = baseline_opts or { "filter" }
|
|
|
|
|
+
|
|
|
|
|
+ if not state.has_mypy then return end
|
|
|
|
|
+
|
|
|
|
|
+ vim.api.nvim_create_autocmd("LspAttach", {
|
|
|
|
|
+ group = group,
|
|
|
|
|
+ callback = function(args)
|
|
|
|
|
+ local client = vim.lsp.get_client_by_id(args.data.client_id)
|
|
|
|
|
+ if not client or client.name ~= "pyrefly" then return end
|
|
|
|
|
+
|
|
|
|
|
+ local bufnr = args.buf
|
|
|
|
|
+ run_mypy(bufnr)
|
|
|
|
|
+
|
|
|
|
|
+ if not vim.b[bufnr].majpeg_active then
|
|
|
|
|
+ vim.b[bufnr].majpeg_active = true
|
|
|
|
|
+ vim.api.nvim_create_autocmd("BufWritePost", {
|
|
|
|
|
+ group = group,
|
|
|
|
|
+ buffer = bufnr,
|
|
|
|
|
+ callback = function() run_mypy(bufnr) end
|
|
|
})
|
|
})
|
|
|
end
|
|
end
|
|
|
- })
|
|
|
|
|
- status.setup = true
|
|
|
|
|
|
|
+ end,
|
|
|
|
|
+ })
|
|
|
end
|
|
end
|
|
|
|
|
|
|
|
-return {
|
|
|
|
|
- setup = setup,
|
|
|
|
|
- toggle = function()
|
|
|
|
|
- status.enabled = not status.enabled;
|
|
|
|
|
- end,
|
|
|
|
|
- status = function()
|
|
|
|
|
- return status
|
|
|
|
|
|
|
+M.toggle = function()
|
|
|
|
|
+ state.enabled = not state.enabled
|
|
|
|
|
+ if not state.enabled then
|
|
|
|
|
+ vim.diagnostic.reset(namespace)
|
|
|
end
|
|
end
|
|
|
-}
|
|
|
|
|
|
|
+end
|
|
|
|
|
+
|
|
|
|
|
+M.status = function() return state end
|
|
|
|
|
+
|
|
|
|
|
+return M
|