A flat, minimal Neovim setup focused on Python/Django with LSP, DAP, Treesitter, blink.cmp, and Conform. PyCharm-style UI: Darcula theme, editor tabs, statusline and gutter change stripes. Debugging also covers go, node, typescript and react.
init.lua # Main entry point
lua/
core/ # Core configuration
options.lua
plugins/ # Modular plugin configurations
init.lua # Plugin loader
core.lua # Core plugins (treesitter, telescope, blink, conform)
ui.lua # UI plugins (theme, bufferline, lualine, gitsigns, nvim-tree)
lsp.lua # LSP plugins
dap.lua # Debugging plugins
tools.lua # Utility plugins
keymaps/ # Categorized keymaps
init.lua # Keymap loader
navigation.lua # Window/tab navigation
search.lua # Telescope and search
editing.lua # Formatting and replace
debug.lua # Debugging keymaps
git.lua # Git operations
tools.lua # Various tool keymaps
lsp/ # Enhanced LSP configuration
init.lua # Main LSP setup
servers/ # Server-specific configurations
python.lua # pyright + ruff (native `ruff server`)
lua.lua # lua_ls
typescript.lua # ts_ls
terraform.lua # terraformls 0.11 compat shim
autocmds/ # Organized autocommands
init.lua # Autocmd loader
filetypes.lua # Filetype-specific settings
session.lua # Session management
ui.lua # UI-related autocommands
config/ # Plugin setup bodies (see the rule below)
blink.lua # blink.cmp completion
telescope.lua # Telescope
treesitter.lua # Treesitter
python_hl.lua # Python-specific highlights
conform.lua # Formatters
dap.lua # DAP adapters and launch configs
dapui.lua # DAP UI
session.lua # auto-session
queries/
html_tags/injections.scm # replaces the bundled query: <script> injects tsx
scripts/
install.sh # From-scratch installer (deps, config, dotfiles, plugins)
brew-export.sh # Dump installed Homebrew packages to system/Brewfile
brew-import.sh # Install everything from system/Brewfile
mac_packs.sh, macos.sh # macOS packages and defaults
display.sh # External monitor color profile (run by hand)
system/ # Dotfiles and tool configs, detailed in system/README.md
zshrc # Shell config (linked to ~/.zshrc)
lazygit.yml # Lazygit config (delta pager)
Brewfile # Full machine package dump
tests/
run.sh # luacheck + headless feature suite (63 checks)
features.lua # The checks: options, UI, LSP, DAP, keymaps, autocmds
e2e.sh # Real-nvim TUI suite driven via tmux (24 checks)
One rule, so a plugin is never configured in two places at once:
lua/plugins/*.luaholds the lazy spec (repo, lazy trigger,opts) and anyconfig/initbody up to about five lines.lua/config/<plugin>.luaholds anything longer, exposingM.setup(), and the spec just calls it.- Autocmds that are not part of a plugin's own setup live in
lua/autocmds/.
Ignoring this is how the *.dbout autocmd ended up registered twice.
- xiantang/darcula-dark.nvim (PyCharm Darcula theme)
- akinsho/bufferline.nvim (editor tabs)
- nvim-lualine/lualine.nvim (statusline)
- lewis6991/gitsigns.nvim (gutter change stripes)
- lazy.nvim
- nvim-lspconfig, mason.nvim, mason-lspconfig.nvim
- blink.cmp (completion, loads on InsertEnter/CmdlineEnter)
- telescope.nvim (plenary.nvim, loads on demand)
- nvim-treesitter
- nvim-treesitter-context (scope context)
- nvim-treesitter-textobjects (af/if/ac/ic, master branch)
- indent-blankline.nvim (indent guides)
- nvim-puppeteer (Python f-string auto-conversion)
- nvim-tree (nvim-web-devicons)
- conform.nvim
- nvim-dap + nvim-dap-ui (nvim-nio) + mason-nvim-dap
- auto-session
- kulala.nvim (HTTP client, pure Lua, no luarocks)
- vim-dadbod + vim-dadbod-ui (SQL client)
- markdown-preview.nvim (random free port)
- github/copilot.vim (bundled language server, no npx)
- vim-maximizer
- f-person/git-blame.nvim (inline blame, 1s delay off the cursor path)
- Servers ensured: pyright, ruff (native
ruff server), lua_ls, ts_ls - mason-lspconfig v2 auto-enables every installed server, so
automatic_enableinlua/lsp/init.luais an explicit allowlist. Add a server there to use it. lua/lsp/servers/*.luamust be required beforemason-lspconfig.setup():vim.lsp.enable()starts clients immediately, and avim.lsp.config()call after that is ignored for the running client- Loads deferred on the first buffer, then re-fires
nvim.lsp.enableso the file opened from the command line also attaches - Buffer-local LSP keymaps via
LspAttach:gd,gr,gi,K,<leader>rn,<leader>ca. Global:<leader>gd(vsplit),<leader>gr(telescope) - terraformls: default on_attach disabled on nvim 0.11 (uses a 0.12-only API)
- Context showing function/class scope at top of window
- Textobjects:
af/if(function),ac/ic(class) - Incremental selection:
gnn,grn,grc,grm - Indent guides (indent-blankline)
- Language parsers: lua, python, javascript, typescript, tsx, html, http, css, json, markdown, bash, vim, go, rust, ruby, toml, yaml, requirements, dockerfile, make, tmux
queries/html_tags/injections.scmreplaces the bundled query so a bare<script>injects tsx (JSX highlights in plain .html). It has to live underhtml_tags/, nothtml/: html_tags is a base lang for html, so a file underqueries/html/can only add rules, never remove the javascript one
Leader is <Space>. Every map carries a desc, so :Telescope keymaps lists
them with their descriptions. Debug keymaps have their own section further down.
| Key | Does | Note |
|---|---|---|
H / L |
go to line start / line end | ^ and $ without the symbol keys |
W / B |
word back / word forward | deliberately swapped from vim's defaults |
<leader>j / <leader>k |
down / up one screen line | gj/gk, moves inside a wrapped line |
| Key | Does | Note |
|---|---|---|
<leader>ww |
save | |
<leader>wq |
save and quit | |
<leader>qq |
quit and throw away changes | :q! |
<leader>ee |
file explorer on/off | nvim-tree |
<leader>ef |
reveal the current file in the explorer | |
<leader>sv / <leader>sh |
split vertical / horizontal | |
<leader>se |
equalize splits, in every tab | |
<leader>sm |
maximize the split, press again to restore | vim-maximizer |
<leader>to / <leader>tn / <leader>tp |
new / next / previous tab | |
<leader>bn / <leader>bp / <leader>bd |
next / previous / close buffer | the tabs bufferline draws on top. Not on <Tab>: in a terminal that is the same byte as <C-i>, and it would kill jumplist-forward |
| Key | Does | Note |
|---|---|---|
<leader>ff |
find files | includes gitignored files (no_ignore) |
<leader>fg |
grep the whole project as you type | e.g. type def index to land on the Django view |
<leader>fb |
pick an open buffer | |
<leader>fo |
symbols of the current file | column widths follow the window width |
| Key | Does | Note |
|---|---|---|
gd / gr / gi / K |
definition / references / implementations / hover | buffer-local, appear only after a server attaches |
<leader>rn / <leader>ca |
rename / code action | buffer-local |
<leader>gd |
definition in a vertical split | splits only after a result comes back, so a miss leaves no empty window |
<leader>gr |
references in telescope |
| Key | Does | Note |
|---|---|---|
<leader>e |
float with the diagnostic under the cursor | |
<leader>E |
the same float, focusable | enter it to yank the message |
[d / ]d |
previous / next diagnostic | |
<leader>gp / <leader>gn |
same as [d / ]d |
| Key | Does | Note |
|---|---|---|
<leader>f |
format the buffer (normal) or the selection (visual) | conform.nvim, no LSP fallback |
<leader>S |
replace the word under the cursor everywhere in the file | fills :%s/\<word\>/word/gI and parks the cursor on the replacement: type the new text and press Enter |
<leader>S (visual) |
same, using the selection |
| Key | Does | Note |
|---|---|---|
<leader>gb |
inline git blame on/off | |
<leader>mp / <leader>mP |
markdown preview start / stop | |
<leader>rr |
run the HTTP request under the cursor | kulala, in .http files |
<leader>db |
database UI on/off | dadbod-ui. Queries run with its own buffer-local <leader>S |
Works in python, go, javascript, typescript and both react filetypes. Any other
filetype has no configuration, and <leader>dc says so instead of starting.
| Key | Does | Example |
|---|---|---|
<leader>bb |
breakpoint on the current line, press again to remove | |
<leader>bc |
breakpoint that only stops when a condition holds | asks for it, answer i == 3 to stop on the 4th pass of a loop |
<leader>bl |
logpoint: prints instead of stopping | asks for the message, answer double {value} and every call prints in the REPL with value filled in |
<leader>ba |
put every breakpoint in the quickfix window | :cclose to close it again |
<leader>br |
delete every breakpoint |
| Key | Does | Note |
|---|---|---|
<leader>dc |
start, or continue when stopped | the first press shows the numbered config menu for the filetype |
<leader>dj |
step over | |
<leader>dk |
step into | steps into the function being called on the current line |
<leader>do |
step out | back to the caller |
<leader>dl |
run the last config again | no menu |
<leader>dt |
terminate the session | UI closes with it |
<leader>dd |
disconnect and close the UI | |
<leader>du |
show / hide the UI | the session keeps running |
Python only:
| Key | Does | Note |
|---|---|---|
<leader>df |
debug the pytest test under the cursor | reads LSP symbols, so TestClass::test_method is resolved for a method |
<leader>dp |
picker with every test in the file | plus a "Manual" entry to type any pytest target |
Commands, once the plugin has loaded: :DapEval (evaluate expressions in a
scratch window), :DapToggleRepl, :DapPause, :DapRestartFrame,
:DapShowLog.
With a file that has a double() helper called inside a loop:
<leader>bcon the loop line, answeri == 3<leader>blon the first line ofdouble, answerdouble {value}<leader>bato confirm both are registered,:cclose<leader>dc, pick1(Launch file); the UI opens and execution stops on the loop only wheniis 3- read
iandtotalin the Scopes panel <leader>dksteps intodouble,<leader>docomes back,<leader>djmoves one line<leader>dcruns to the end; the logpoint lines are waiting in the REPL<leader>dtends it,<leader>dlruns the same config again without asking
Left column: scopes, watches, breakpoints. Right column: REPL and console. Both scale with the terminal width. It opens and closes with the session.
Inside any panel: <CR> expands, e edits a value, d removes an entry, r
sends the entry to the REPL, o opens, t toggles. The watches panel is a
prompt buffer: press i and type an expression such as total * 10. The REPL
takes expressions the same way.
- mason-nvim-dap ensures debugpy (python), delve (go) and js-debug-adapter (node, typescript, react). They install on the first debug session, not at install time
- Python: Launch file, Django runserver, Pytest file. The adapter uses mason's debugpy when installed, otherwise the python of the active venv
- Go: Launch file, Launch package, Test package, Attach to process. delve gets a 20s initialize timeout because it compiles the program before answering
- Node, typescript and react: Launch file, Attach to process, Attach to node
port 9229, Launch Chrome on dev server (asks for the URL, default
http://localhost:5173), Attach to Chrome port 9222. Launching a.tsfile needs no ts-node or tsx: node 22.18+ strips the types itself - The same js-debug-adapter serves node and chrome. React components only stop on the chrome configs; the node ones cannot reach browser code
- Known upstream noise: terminating a python session while it sits on a breakpoint makes debugpy SIGKILL the debuggee, and its adapter then exits 1, so nvim-dap warns. Letting the program finish, or terminating it while it runs, exits clean. delve and js-debug never do this
- Auto-restores sessions on startup (auto-session defaults)
- NvimTree opens only if no session was restored
<leader>fuses conform.nvim (no LSP fallback)- Formatters: Python
ruff_format, Luastylua, Cclang_format, JS/TS/JSON/CSS/HTML/YAML/Markdownprettier(reads.prettierrc.json) - All of them come from mason; a formatter that is not installed is a silent
no-op, so check with
:ConformInfoif<leader>fseems to do nothing
- Startup ~43ms: telescope and treesitter load on first use, not at boot.
blink.cmp is the exception:
lua/lsp/init.luarequires it so its plugin file registers LSP capabilities before any server starts, so it loads on the first buffer, not on InsertEnter - Unused providers disabled (python3, ruby, perl, node)
- git-blame virtual text delayed 1s so it stays off the cursor path
- No lazyredraw (Neovim marks it unsupported; it causes stutter)
./tests/run.sh # luacheck, then 63 headless feature checks; nonzero on failure
./tests/e2e.sh # real nvim TUI in tmux: real keystrokes, rendered screen,
# full debug session; requires tmuxFrom scratch on a new machine (macOS or Ubuntu/Debian):
curl -fsSL https://raw.githubusercontent.com/iklobato/iklobato-nvim-config-python/main/scripts/install.sh | bashOr from a local checkout:
git clone https://github.com/iklobato/iklobato-nvim-config-python.git ~/.config/nvim
~/.config/nvim/scripts/install.shPass --no-dotfiles to install Neovim only and leave ~/.zshrc and lazygit
untouched. Anything it replaces (~/.config/nvim, ~/.zshrc, lazygit config)
is moved to a timestamped .bak_<date> first.
What it sets up:
- Dependencies: Neovim 0.11+, git, Node 22+, ripgrep, python3, git-delta (lazygit pager), Meslo LG Nerd Font
- Config: this repo at
~/.config/nvim - Dotfiles:
system/zshrc→~/.zshrcandsystem/lazygit.yml→ the platform lazygit path, plus oh-my-zsh and the zsh-syntax-highlighting plugin the zshrc expects - Plugins:
Lazy! syncheadless, then mason installs pyright, ruff, lua_ls, ts_ls, stylua and debugpy. delve and js-debug-adapter are not installed here: mason-nvim-dap pulls them the first time you open a debug session
system/Brewfile is not installed by the script: it's a full machine dump.
Use ./scripts/brew-import.sh if you want it.
After install, set your terminal font to "MesloLGS Nerd Font" so icons render.
- macOS or Ubuntu/Debian (other systems: install deps manually, then run the script)
- Neovim 0.11+
- Python 3 (for LSP and DAP)
- Node.js 22.18+ and ripgrep (for Telescope, LSP servers, Copilot, and the node/typescript debugger)
- Go (only to debug Go: mason builds delve with the local toolchain)
- tmux (only for tests/e2e.sh)