Bladeren bron

feat(neovim): improve Python LSP integrations

Joe 2 maanden geleden
bovenliggende
commit
e48f834469

+ 177 - 89
.config/nvim/custom/maj-peg/lua/maj-peg/init.lua

@@ -1,104 +1,192 @@
 local Job = require("plenary.job")
 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,
     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
-    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
-    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
-                        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
+
+                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
-        })
-    status.setup = true
+        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
+
+M.status = function() return state end
+
+return M

+ 0 - 75
.config/nvim/lsp/basedpyright.lua

@@ -1,75 +0,0 @@
---- @type vim.lsp.ClientConfig
-return {
-    cmd = { 'basedpyright-langserver', '--stdio' },
-    filetypes = { 'python' },
-    root_markers = {
-        'pyproject.toml',
-        'setup.py',
-        'setup.cfg',
-        'requirements.txt',
-        'Pipfile',
-        'pyrightconfig.json',
-    },
-    single_file_support = true,
-    settings = {
-        basedpyright = {
-            analysis = {
-                typeCheckingMode = "standard",
-                diagnosticMode = "openFilesOnly",
-                autoSearchPaths = true,
-                include = { "*" },
-                exclude = { "**/node_modules", "**/__pycache__", "**/build" },
-                useLibraryCodeForTypes = true,
-                analyzeUnannotatedFunctions = true,
-                reportUnreachable = true,
-            },
-            openFilesOnly = true,
-            autoImportCompletions = true,
-            disableOrganizeImports = true,
-        }
-    },
-    on_init = function(client, _)
-        local typecheckingPicker = function(opts)
-            local pickers = require("telescope.pickers")
-            local themes = require("telescope.themes")
-            local actions = require("telescope.actions")
-            local action_state = require("telescope.actions.state")
-            local finders = require("telescope.finders")
-            local conf = require("telescope.config").values
-            local current = client.settings.basedpyright.analysis
-                .typeCheckingMode
-            if current == nil then current = "unset" end
-            pickers.new(themes.get_dropdown({}), {
-                prompt_title = "BasedPyright Typechecking (Current: " ..
-                    current .. ")",
-                finder = finders.new_table({
-                    results = vim.tbl_filter(
-                        function(r) return r ~= current end,
-                        { "off", "basic", "standard", "strict", "all" }),
-                }),
-                sorter = conf.generic_sorter(opts),
-                attach_mappings = function(prompt_bufnr, _)
-                    actions.select_default:replace(function()
-                        actions.close(prompt_bufnr)
-                        local selection = action_state
-                            .get_selected_entry()
-                        client.settings.basedpyright.analysis.typeCheckingMode =
-                            selection.value
-                        client:notify(
-                            "workspace/didChangeConfiguration",
-                            client.settings)
-                    end)
-                    return true
-                end,
-            }):find()
-        end
-
-        if pcall(require, "command-palette") and pcall(require, "telescope.pickers") then
-            require("command-palette").add({ {
-                "BasedPyright Typechecking",
-                "Set `typeCheckingMode` of BasedPyright",
-                typecheckingPicker }
-            })
-        end
-    end,
-}

+ 35 - 0
.config/nvim/lsp/pyrefly.lua

@@ -0,0 +1,35 @@
+--- @type vim.lsp.ClientConfig
+return {
+    cmd = {
+        'pyrefly',
+        'lsp',
+    },
+    filetypes = {
+        'python',
+    },
+    root_markers = {
+        '.git',
+        'pyproject.toml',
+        'pyrefly.toml',
+        'requirements.txt',
+        ".venv",
+    },
+    single_file_support = true,
+    settings = {
+        python = {
+            pyrefly = {
+                displayTypeErrors = 'force-on',
+                disableLanguageServices = false,
+                analysis = {
+                    inlayHints = {
+                        callArgumentNames = true,
+                        functionReturnTypes = true,
+                        pytestParameters = true,
+                        variableTypes = true,
+                    },
+                    showHoverGoToLinks = true,
+                },
+            },
+        }
+    },
+}

+ 17 - 14
.config/nvim/lsp/ruff.lua

@@ -1,18 +1,21 @@
 --- @type vim.lsp.ClientConfig
 return {
-    cmd = { 'ruff', 'server' },
-    filetypes = { 'python' },
-    root_markers = {
-        'pyproject.toml',
-        'setup.py',
-        'setup.cfg',
-        'requirements.txt',
-        'Pipfile',
-        'pyrightconfig.json',
+    cmd = { "ruff", "server" },
+    filetypes = { "python" },
+    root_markers = { ".git", "pyproject.toml", "ruff.toml", ".ruff.toml", ".venv", },
+
+    init_options = {
+        settings = {
+            lint = {
+                enable = true,
+                ignore = { "F841", "F821", "F822", "F823" },
+            },
+            codeAction = {
+                fixViolation = { enable = true },
+            },
+            fixAll = true,
+            organizeImports = true,
+            showSyntaxErrors = false,
+        },
     },
-    single_file_support = true,
-    settings = {},
-    on_attach = function(client, _)
-        client.server_capabilities.hoverProvider = false
-    end
 }