{ config, pkgs, lib, ... }: let inherit (builtins) attrNames; inherit (lib.options) mkEnableOption mkOption literalExpression; inherit (lib.lists) flatten; inherit (lib) genAttrs; inherit (lib.meta) getExe getExe'; inherit (lib.modules) mkIf mkMerge; inherit (lib.types) enum package bool listOf; inherit (lib.nvim.attrsets) mapListToAttrs; inherit (lib.nvim.types) deprecatedSingleOrListOf diagnostics; inherit (lib.trivial) warn; cfg = config.vim.languages.python; defaultServers = ["basedpyright"]; servers = ["pyrefly" "pyright" "basedpyright" "python-lsp-server" "ruff" "ty" "zuban"]; defaultFormat = ["black"]; formats = { black = { command = getExe pkgs.black; }; isort = { command = getExe pkgs.isort; }; # dummy option for backwards compat black-and-isort = {}; ruff = { command = getExe pkgs.ruff; args = ["format" "-"]; }; ruff-check = { package = pkgs.writeShellApplication { name = "ruff-check"; runtimeInputs = [pkgs.ruff]; text = '' ruff check --fix --exit-zero - ''; }; }; }; defaultDebugger = "debugpy"; debuggers = { debugpy = { # idk if this is the best way to install/run debugpy package = pkgs.python3.withPackages (ps: with ps; [debugpy]); dapConfig = '' dap.adapters.debugpy = function(cb, config) if config.request == 'attach' then ---@diagnostic disable-next-line: undefined-field local port = (config.connect or config).port ---@diagnostic disable-next-line: undefined-field local host = (config.connect or config).host or '127.0.0.1' cb({ type = 'server', port = assert(port, '`connect.port` is required for a python `attach` configuration'), host = host, options = { source_filetype = 'python', }, }) else cb({ type = 'executable', command = '${getExe cfg.dap.package}', args = { '-m', 'debugpy.adapter' }, options = { source_filetype = 'python', }, }) end end dap.configurations.python = { { -- The first three options are required by nvim-dap type = 'debugpy'; -- the type here established the link to the adapter definition: `dap.adapters.debugpy` request = 'launch'; name = "Launch file"; -- Options below are for debugpy, see https://github.com/microsoft/debugpy/wiki/Debug-configuration-settings for supported options program = "''${file}"; -- This configuration will launch the current file if used. pythonPath = function() -- debugpy supports launching an application with a different interpreter then the one used to launch debugpy itself. -- The code below looks for a `venv` or `.venv` folder in the current directly and uses the python within. -- You could adapt this - to for example use the `VIRTUAL_ENV` environment variable. local cwd = vim.fn.getcwd() if vim.fn.executable(cwd .. '/venv/bin/python') == 1 then return cwd .. '/venv/bin/python' elseif vim.fn.executable(cwd .. '/.venv/bin/python') == 1 then return cwd .. '/.venv/bin/python' elseif vim.fn.executable("python") == 1 then return vim.fn.exepath("python") else -- WARNING cfg.dap.package probably has NO libraries other than builtins and debugpy return '${getExe cfg.dap.package}' end end; }, } ''; }; }; defaultDiagnosticsProvider = ["mypy"]; diagnosticsProviders = { mypy = { config = { cmd = getExe' pkgs.mypy "mypy"; }; }; }; in { options.vim.languages.python = { enable = mkEnableOption "Python language support"; treesitter = { enable = mkEnableOption "Python treesitter" // { default = config.vim.languages.enableTreesitter; defaultText = literalExpression "config.vim.languages.enableTreesitter"; }; package = mkOption { description = "Python treesitter grammar to use"; type = package; default = pkgs.vimPlugins.nvim-treesitter.grammarPlugins.python; }; }; lsp = { enable = mkEnableOption "Python LSP support" // { default = config.vim.lsp.enable; defaultText = literalExpression "config.vim.lsp.enable"; }; servers = mkOption { type = listOf (enum servers); default = defaultServers; description = "Python LSP server to use"; }; }; format = { enable = mkEnableOption "Python formatting" // { default = config.vim.languages.enableFormat; defaultText = literalExpression "config.vim.languages.enableFormat"; }; type = mkOption { type = deprecatedSingleOrListOf "vim.language.python.format.type" (enum (attrNames formats)); default = defaultFormat; description = "Python formatters to use"; }; }; # TODO this implementation is very bare bones, I don't know enough python to implement everything dap = { enable = mkOption { type = bool; default = config.vim.languages.enableDAP; defaultText = literalExpression "config.vim.languages.enableDAP"; description = "Enable Python Debug Adapter"; }; debugger = mkOption { type = enum (attrNames debuggers); default = defaultDebugger; description = "Python debugger to use"; }; package = mkOption { type = package; default = debuggers.${cfg.dap.debugger}.package; example = literalExpression "with pkgs; python39.withPackages (ps: with ps; [debugpy])"; description = '' Python debugger package. This is a python package with debugpy installed, see https://nixos.wiki/wiki/Python#Install_Python_Packages. ''; }; }; extraDiagnostics = { enable = mkEnableOption "extra Python diagnostics" // { default = config.vim.languages.enableExtraDiagnostics; defaultText = literalExpression "config.vim.languages.enableExtraDiagnostics"; }; types = diagnostics { langDesc = "Python"; inherit diagnosticsProviders; inherit defaultDiagnosticsProvider; }; }; }; config = mkIf cfg.enable (mkMerge [ (mkIf cfg.treesitter.enable { vim.treesitter.enable = true; vim.treesitter.grammars = [cfg.treesitter.package]; }) (mkIf cfg.lsp.enable { vim.lsp = { presets = genAttrs cfg.lsp.servers (_: {enable = true;}); servers = genAttrs cfg.lsp.servers (_: { filetypes = ["python"]; root_markers = [ "Pipfile" "pyproject.toml" "requirements.txt" "setup.cfg" "setup.py" ]; }); }; }) (mkIf cfg.format.enable { vim.formatter.conform-nvim = let names = flatten (map (type: if type == "black-and-isort" then warn '' vim.languages.python.format.type: "black-and-isort" is deprecated, use `["black" "isort"]` instead. '' ["black" "isort"] else type) cfg.format.type); in { enable = true; setupOpts = { formatters_by_ft.python = names; formatters = mapListToAttrs (name: { inherit name; value = formats.${name}; }) names; }; }; }) (mkIf cfg.dap.enable { vim.debugger.nvim-dap.enable = true; vim.debugger.nvim-dap.sources.python-debugger = debuggers.${cfg.dap.debugger}.dapConfig; }) (mkIf cfg.extraDiagnostics.enable { vim.diagnostics.nvim-lint = { enable = true; linters_by_ft.python = cfg.extraDiagnostics.types; linters = mkMerge (map (name: {${name} = diagnosticsProviders.${name}.config;}) cfg.extraDiagnostics.types); }; }) ]); }