Skip to content

Instantly share code, notes, and snippets.

@alexandremcosta
Created February 24, 2026 19:37
Show Gist options
  • Select an option

  • Save alexandremcosta/17938d8ffd183efb22eb278638dd116c to your computer and use it in GitHub Desktop.

Select an option

Save alexandremcosta/17938d8ffd183efb22eb278638dd116c to your computer and use it in GitHub Desktop.
A single-file Neovim configuration inspired by AstroNvim's UX conventions with a minimal, readable setup.
-- =============================================================================
-- A single-file Neovim configuration inspired by AstroNvim's UX conventions
-- with a minimal, readable setup.
-- =============================================================================
--
-- REQUIREMENTS:
-- Neovim >= 0.11 (uses vim.lsp.config/enable API and LspProgress event)
-- git (lazy.nvim bootstrap and gitsigns)
-- make (telescope-fzf-native build step)
-- A Nerd Font (icons in neo-tree, lualine, bufferline, dashboard)
-- =============================================================================
--
--
-- ## Table of Contents
--
-- - [Getting Started](#getting-started)
-- - [Dependencies](#dependencies)
-- - [Editor Settings](#editor-settings)
-- - [Plugins](#plugins)
-- - [Keymappings](#keymappings)
-- - [Leader & Local Leader](#leader--local-leader)
-- - [General / Editing](#general--editing)
-- - [Windows & Splits](#windows--splits)
-- - [Buffers](#buffers)
-- - [File Explorer](#file-explorer)
-- - [Telescope / Fuzzy Finding](#telescope--fuzzy-finding)
-- - [LSP](#lsp)
-- - [Git](#git)
-- - [Terminal](#terminal)
-- - [Elixir / Mix](#elixir--mix)
-- - [Sessions](#sessions)
-- - [Packages & Plugins](#packages--plugins)
-- - [Quickfix & Location Lists](#quickfix--location-lists)
-- - [UI Toggles](#ui-toggles)
-- - [Comments](#comments)
-- - [Completion](#completion)
-- - [Autocmds & Special Behaviors](#autocmds--special-behaviors)
-- - [Adding Language Support](#adding-language-support)
-- - [Power User Tips](#power-user-tips)
--
-- ---
--
-- ## Getting Started
--
-- ### How to Use
--
-- ```sh
-- # Copy config
-- cp init.lua ~/.config/alexanvim/init.lua
--
-- # Launch
-- NVIM_APPNAME=alexanvim nvim
--
-- # Or add an alias to ~/.zshrc
-- alias an='NVIM_APPNAME=alexanvim nvim'
-- ```
--
-- ### First Run
--
-- Plugin manager [lazy.nvim](https://github.com/folke/lazy.nvim) is auto-bootstrapped on first launch. On first open, run `:Lazy sync` if plugins do not install automatically.
--
-- ```
-- :Mason " manage LSP servers, formatters, linters
-- :MasonInstall lua-language-server stylua
-- :TSInstall <language> " install treesitter parsers
-- :checkhealth " verify the environment
-- ```
--
-- ### Dependencies
--
-- **Required:**
--
-- - `git` — lazy.nvim bootstrapping and gitsigns
-- - `make` — telescope-fzf-native compilation
-- - A **Nerd Font** — icons throughout the UI
--
-- **Recommended:**
--
-- - `ripgrep` (`rg`) — fast live grep (`<Space>fw`)
-- - `fd` — faster file finding (`<Space>ff`)
-- - `lazygit` — git UI (`<Space>gg`)
--
-- **Optional:**
--
-- - `node` / `npm` — Mason-managed LSP servers
-- - `claude` CLI — Claude AI terminal (`<Space>tc`)
--
-- **Elixir:**
--
-- - `expert` binary at `/usr/local/bin/expert` — Elixir LSP server
--
-- ---
--
-- ## Editor Settings
--
-- | Option | Value | Effect |
-- |--------|-------|--------|
-- | `number` | true | Show absolute line numbers |
-- | `relativenumber` | true | Show relative line numbers |
-- | `signcolumn` | "yes" | Always show sign column (prevents layout shift) |
-- | `wrap` | false | Disable line wrap |
-- | `tabstop` / `shiftwidth` | 2 | 2-space indentation |
-- | `expandtab` | true | Spaces instead of tabs |
-- | `smartindent` | true | Smart auto-indentation |
-- | `termguicolors` | true | True 24-bit color |
-- | `mouse` | "a" | Mouse support in all modes |
-- | `clipboard` | "unnamedplus" | System clipboard integration |
-- | `splitright` / `splitbelow` | true | New splits open right/below |
-- | `ignorecase` + `smartcase` | true | Case-insensitive unless uppercase is used |
-- | `undofile` | true | Persistent undo across sessions |
-- | `scrolloff` | 8 | Keep 8 lines visible above/below cursor |
-- | `updatetime` | 200ms | Faster gitsigns/diagnostics refresh |
-- | `timeoutlen` | 300ms | Faster which-key popup |
-- | `cursorline` | true | Highlight current line |
-- | `showmode` | false | Mode shown in statusline instead |
-- | `laststatus` | 3 | Global statusline (shared across all windows) |
--
-- ---
--
-- ## Plugins
--
-- | Plugin | Purpose |
-- |--------|---------|
-- | **lazy.nvim** | Plugin manager |
-- | **astrotheme** | Colorscheme (astrodark style) |
-- | **which-key.nvim** | Keybinding popup (press `<Space>` and wait) |
-- | **lualine.nvim** | Statusline with mode, branch, diagnostics, LSP |
-- | **bufferline.nvim** | Buffer tabs with diagnostic indicators |
-- | **dashboard-nvim** | Home screen with quick actions |
-- | **neo-tree.nvim** | File explorer (hijacks netrw) |
-- | **telescope.nvim** | Fuzzy finder for files, grep, buffers, etc. |
-- | **nvim-lspconfig** | LSP client configuration |
-- | **mason.nvim** | LSP/tool installer |
-- | **mason-lspconfig.nvim** | Auto-installs `lua_ls` via Mason |
-- | **blink.cmp** | Autocompletion engine |
-- | **LuaSnip** | Snippet engine |
-- | **friendly-snippets** | VSCode-style snippet collection |
-- | **nvim-treesitter** | Syntax parsing and highlighting |
-- | **vim-illuminate** | Highlights all occurrences of word under cursor |
-- | **indent-blankline.nvim** | Vertical indent guide lines |
-- | **todo-comments.nvim** | Highlights `TODO`, `FIXME`, `HACK`, etc. |
-- | **gitsigns.nvim** | Git diff signs in the gutter |
-- | **toggleterm.nvim** | Floating/split terminals |
-- | **nvim-autopairs** | Auto-close brackets and quotes |
-- | **better-escape.nvim** | Exit insert mode with `jj` or `jk` |
-- | **Comment.nvim** | Toggle comments |
-- | **smart-splits.nvim** | Window navigation/resizing with tmux awareness |
-- | **persistence.nvim** | Session save/restore |
-- | **plenary.nvim** | Utility library (dependency) |
-- | **nvim-web-devicons** | File type icons (requires Nerd Font) |
--
-- ---
--
-- ## Keymappings
--
-- ### Leader & Local Leader
--
-- | Key | Role |
-- |-----|------|
-- | `<Space>` | Leader key |
-- | `,` | Local leader key |
--
-- Press `<Space>` and wait to see a which-key popup with all available mappings.
--
-- ---
--
-- ### General / Editing
--
-- | Mapping | Mode | Action |
-- |---------|------|--------|
-- | `<Space>w` | Normal | Save file |
-- | `<Space>q` | Normal | Smart close (window if split exists, else buffer) |
-- | `<Space>n` | Normal | New file |
-- | `<Space>R` | Normal | Rename current file |
-- | `<Space>h` | Normal | Open dashboard (home) |
-- | `<Space>ev` | Normal | Edit init.lua |
-- | `<Space>sv` | Normal | Source/reload init.lua |
-- | `<Esc>` | Normal | Clear search highlight |
-- | `jj` / `jk` | Insert | Escape to normal mode |
-- | `J` | Visual | Move selection down |
-- | `K` | Visual | Move selection up |
--
-- ---
--
-- ### Windows & Splits
--
-- | Mapping | Action |
-- |---------|--------|
-- | `\` | Horizontal split |
-- | `\|` | Vertical split |
-- | `<C-h>` | Move to left window |
-- | `<C-j>` | Move to below window |
-- | `<C-k>` | Move to above window |
-- | `<C-l>` | Move to right window |
-- | `<leader><Up>` | Resize window up |
-- | `<leader><Down>` | Resize window down |
-- | `<leader><Left>` | Resize window left |
-- | `<leader><Right>` | Resize window right |
--
-- Window navigation is tmux-aware via smart-splits: works seamlessly when Neovim is inside a tmux pane.
--
-- ---
--
-- ### Buffers
--
-- | Mapping | Action |
-- |---------|--------|
-- | `]b` | Next buffer |
-- | `[b` | Previous buffer |
-- | `<Space>c` | Close current buffer |
-- | `<Space>bb` | Pick buffer (Telescope) |
-- | `<Space>bc` | Close all other buffers |
--
-- ---
--
-- ### File Explorer
--
-- | Mapping | Action |
-- |---------|--------|
-- | `<Space>e` | Toggle neo-tree |
-- | `<Space>o` | Focus neo-tree |
--
-- Neo-tree auto-follows the current file and hijacks netrw directory browsing.
--
-- ---
--
-- ### Telescope / Fuzzy Finding
--
-- | Mapping | Action |
-- |---------|--------|
-- | `<Space>ff` | Find files |
-- | `<Space>fF` | Find files (include hidden) |
-- | `<Space>fw` | Live grep |
-- | `<Space>fW` | Live grep (include hidden) |
-- | `<Space>fb` | Buffers |
-- | `<Space>fo` | Old/recent files |
-- | `<Space>fc` | Find word at cursor |
-- | `<Space>fC` | Commands |
-- | `<Space>fh` | Help tags |
-- | `<Space>fk` | Keymaps |
-- | `<Space>fm` | Man pages |
-- | `<Space>fr` | Registers |
-- | `<Space>ft` | Colorschemes |
-- | `<Space>f'` | Marks |
--
-- ---
--
-- ### LSP
--
-- These mappings are active only in buffers with an attached LSP server.
--
-- | Mapping | Action |
-- |---------|--------|
-- | `K` | Hover documentation |
-- | `gd` | Go to definition |
-- | `gD` | Go to declaration |
-- | `gy` | Go to type definition |
-- | `gri` | Go to implementation |
-- | `grr` | Find references |
-- | `grn` | Rename symbol |
-- | `gra` | Code action |
-- | `gl` | Line diagnostics (float) |
-- | `]d` | Next diagnostic |
-- | `[d` | Previous diagnostic |
-- | `<Space>lf` | Format document |
-- | `<Space>lr` | Rename symbol |
-- | `<Space>la` | Code action |
-- | `<Space>lh` | Signature help |
-- | `<Space>li` | LSP info |
-- | `<Space>ld` | Line diagnostics |
-- | `<Space>ls` | Document symbols (Telescope) |
-- | `<Space>lG` | Workspace symbols (Telescope) |
-- | `<Space>lD` | All diagnostics (Telescope) |
-- | `<Space>lR` | References (Telescope) |
--
-- **Configured LSP servers:**
-- - `lua_ls` — Lua (installed/managed by Mason)
-- - `expert` — Elixir LSP at `/usr/local/bin/expert`
--
-- ---
--
-- ### Git
--
-- | Mapping | Action |
-- |---------|--------|
-- | `<Space>gg` | Open Lazygit (floating terminal) |
-- | `<Space>gb` | Git branches (Telescope) |
-- | `<Space>gc` | Git commits (Telescope) |
-- | `<Space>gC` | Git commits for current file (Telescope) |
-- | `<Space>gt` | Git status (Telescope) |
-- | `]c` | Next hunk |
-- | `[c` | Previous hunk |
-- | `<Space>ghs` | Stage hunk |
-- | `<Space>ghr` | Reset hunk |
-- | `<Space>ghS` | Stage buffer |
-- | `<Space>ghR` | Reset buffer |
-- | `<Space>ghp` | Preview hunk |
-- | `<Space>ghb` | Blame current line |
-- | `<Space>ghd` | Diff this file |
--
-- Git signs in the gutter: `▎` for added/changed lines, `▁` for deleted lines.
--
-- ---
--
-- ### Terminal
--
-- | Mapping | Action |
-- |---------|--------|
-- | `<M-&#96;>` | Toggle floating terminal (Alt + Backtick) |
-- | `<Space>tf` | Open float terminal |
-- | `<Space>th` | Open horizontal terminal |
-- | `<Space>tv` | Open vertical terminal |
-- | `<Space>tl` | Open Lazygit in terminal |
-- | `<Space>tc` | Open Claude AI terminal |
--
-- Default terminal direction is floating with rounded borders.
--
-- ---
--
-- ### Elixir / Mix
--
-- | Mapping | Action |
-- |---------|--------|
-- | `<Space>mt` | `mix test` at cursor line |
-- | `<Space>mT` | `mix test` entire file |
-- | `<Space>ms` | `mix test --stale` |
-- | `<Space>mf` | `mix test --failed` |
-- | `<Space>mq` | `mix test --failed` → quickfix list |
--
-- ---
--
-- ### Sessions
--
-- Sessions are auto-saved and restored via persistence.nvim.
--
-- | Mapping | Action |
-- |---------|--------|
-- | `<Space>Ss` | Save session |
-- | `<Space>Sl` | Load last session |
-- | `<Space>S.` | Load session for current directory |
-- | `<Space>Sd` | Stop session persistence |
--
-- ---
--
-- ### Packages & Plugins
--
-- | Mapping | Action |
-- |---------|--------|
-- | `<Space>pm` | Open Mason (LSP/tool installer) |
-- | `<Space>ps` | Plugin status (Lazy) |
-- | `<Space>pS` | Sync plugins |
-- | `<Space>pu` | Check for plugin updates |
-- | `<Space>pU` | Update plugins |
--
-- ---
--
-- ### Quickfix & Location Lists
--
-- | Mapping | Action |
-- |---------|--------|
-- | `<Space>xq` | Open quickfix list |
-- | `<Space>xl` | Open location list |
-- | `]q` | Next quickfix item |
-- | `[q` | Previous quickfix item |
-- | `]l` | Next location item |
-- | `[l` | Previous location item |
--
-- ---
--
-- ### UI Toggles
--
-- | Mapping | Action |
-- |---------|--------|
-- | `<Space>un` | Toggle line numbers (relative & absolute) |
-- | `<Space>uw` | Toggle line wrap |
-- | `<Space>us` | Toggle spellcheck |
-- | `<Space>ud` | Toggle diagnostics |
-- | `<Space>uf` | Toggle autoformat (buffer-local) |
-- | `<Space>uF` | Toggle autoformat (global) |
-- | `<Space>uh` | Toggle inlay hints |
-- | `<Space>ub` | Toggle background (dark/light) |
--
-- ---
--
-- ### Comments
--
-- | Mapping | Mode | Action |
-- |---------|------|--------|
-- | `<Space>/` | Normal | Toggle comment on line |
-- | `<Space>/` | Visual | Toggle comment on selection |
-- | `gcc` | Normal | Toggle comment on line |
-- | `gc` | Normal/Visual | Toggle comment (motion/selection) |
-- | `gbc` | Normal | Toggle block comment on line |
-- | `gb` | Normal/Visual | Toggle block comment (motion/selection) |
--
-- ---
--
-- ### Completion
--
-- Completion is powered by blink.cmp with sources: LSP, path, snippets, buffer.
--
-- | Mapping | Mode | Action |
-- |---------|------|--------|
-- | `<C-Space>` | Insert | Open completion menu |
-- | `<Tab>` | Insert | Next item / forward through snippet |
-- | `<S-Tab>` | Insert | Previous item / backward through snippet |
-- | `<C-e>` | Insert | Cancel/close completion |
-- | `<C-d>` | Insert | Scroll docs down |
-- | `<C-u>` | Insert | Scroll docs up |
--
-- ---
--
-- ## Autocmds & Special Behaviors
--
-- | Behavior | Trigger | Description |
-- |----------|---------|-------------|
-- | Highlight yank | `TextYankPost` | Briefly flashes yanked text |
-- | Restore cursor | `BufReadPost` | Restores cursor to last position when reopening a file |
-- | Auto-resize splits | `VimResized` | Equalizes window sizes when terminal is resized |
-- | Close utility windows | `q` keymap | Closes help, lspinfo, man, quickfix, checkhealth, lazy, mason, dashboard with `q` |
-- | Auto-format on save | `BufWritePre` | Formats via LSP before writing (toggleable) |
-- | LSP keymap attach | `LspAttach` | Attaches all LSP keymaps to the buffer automatically |
-- | Treesitter highlight | `FileType` | Enables treesitter highlighting for all supported filetypes |
-- | LSP status spinner | `LspProgress` | Animated spinner in statusline while LSP initializes; turns green when ready |
--
-- ---
--
-- ## Adding Language Support
--
-- Adding a new language requires three steps: a Treesitter parser, an LSP server, and (optionally) a formatter.
--
-- ### 1. Install the Treesitter parser
--
-- ```
-- :TSInstall <language>
-- ```
--
-- Examples: `python`, `typescript`, `rust`, `go`, `json`, `yaml`, `markdown`.
--
-- Treesitter handles syntax highlighting and structural awareness. After installing, highlighting activates automatically for that filetype.
--
-- ### 2. Install and enable an LSP server
--
-- **Option A — Mason-managed server** (most languages):
--
-- ```
-- :MasonInstall <server-name>
-- ```
--
-- Then register and enable it in `init.lua`, inside the `nvim-lspconfig` `config` function, following the existing pattern:
--
-- ```lua
-- vim.lsp.config("pyright", {
-- capabilities = capabilities,
-- -- optional: extra settings go here
-- })
-- vim.lsp.enable("pyright")
-- ```
--
-- To auto-install the server on first launch, add its name to `ensure_installed` in `mason-lspconfig`:
--
-- ```lua
-- opts = {
-- ensure_installed = { "lua_ls", "pyright" },
-- }
-- ```
--
-- Common server names by language:
--
-- | Language | Mason package | Server name |
-- |------------|--------------------|----------------|
-- | Python | `pyright` | `pyright` |
-- | TypeScript | `typescript-language-server` | `ts_ls` |
-- | Rust | `rust-analyzer` | `rust_analyzer`|
-- | Go | `gopls` | `gopls` |
-- | Ruby | `solargraph` | `solargraph` |
-- | C/C++ | `clangd` | `clangd` |
-- | CSS/HTML | `css-lsp`, `html-lsp` | `cssls`, `html` |
--
-- **Option B — Custom/external binary** (like the Elixir `expert` LSP):
--
-- ```lua
-- vim.lsp.config("my_server", {
-- cmd = { "/path/to/binary", "--stdio" },
-- filetypes = { "mylang" },
-- root_markers = { "my_project_file", ".git" },
-- capabilities = capabilities,
-- })
-- vim.lsp.enable("my_server")
-- ```
--
-- ### 3. Install a formatter (optional)
--
-- Most LSP servers include formatting. If you need a standalone formatter (e.g. `prettier`, `black`, `rustfmt`):
--
-- ```
-- :MasonInstall prettier
-- ```
--
-- Then wire it up with `conform.nvim` or call it directly via a `BufWritePre` autocmd. The existing auto-format-on-save behavior uses `vim.lsp.buf.format`, so any server that supports `textDocument/formatting` works automatically.
--
-- ### Verify everything works
--
-- ```
-- :checkhealth " general environment check
-- :LspInfo " active LSP clients for current buffer
-- :TSInstallInfo " installed parsers
-- :Mason " installed tools
-- ```
--
-- ---
--
-- ## Power User Tips
--
-- These are features already available in this config that are easy to overlook. Each tip includes a concrete workflow where it pays off.
--
-- ---
--
-- ### Vim Marks — instant file teleportation
--
-- Marks let you jump back to an exact line instantly, without Telescope or search.
--
-- | Command | Action |
-- |---------|--------|
-- | `ma` | Set mark `a` at current position |
-- | `` `a `` | Jump to exact line **and column** of mark `a` |
-- | `'a` | Jump to the line of mark `a` (first non-blank) |
-- | `''` | Jump back to where you were before the last jump |
-- | `<Space>f'` | Browse all marks with Telescope |
--
-- **Workflow:** You're deep in a Phoenix controller and need to cross-reference a schema field, then return. Set `ms` on the controller line, jump to the schema, look it up, hit `` `s `` to snap back to the exact character you left.
--
-- Use **uppercase marks** (`mA`–`mZ`) for cross-file anchors — they survive buffer changes and sessions. Set `mA` in your main router file and you can jump there from anywhere with `` `A ``.
--
-- ---
--
-- ### Quickfix List — batch navigation across files
--
-- The quickfix list is a persistent, navigable list of locations. Many operations populate it automatically.
--
-- **How it gets populated:**
--
-- - `grr` / `<Space>lR` — LSP references land in quickfix
-- - `<Space>fw` (Telescope live grep) — press `<C-q>` inside Telescope to send all results to quickfix
-- - `:vimgrep /pattern/ **/*.ex` — manual grep across files
-- - Mix test output (failures include file:line references you can load with `:cfile`)
--
-- **Navigation:**
--
-- | Command | Action |
-- |---------|--------|
-- | `<Space>xq` | Open quickfix window |
-- | `]q` / `[q` | Next / previous item |
-- | `<C-q>` | (in Telescope) Send results to quickfix |
--
-- **Workflow:** You're renaming a function used in 12 files. Run `grr` to find all references, they open in Telescope. Press `<C-q>` to dump them all to quickfix. Now `]q` / `[q` walks you through every usage site. Make your edits, close the quickfix when done.
--
-- ---
--
-- ### `<Space>fc` — search the word under your cursor
--
-- `<Space>fc` runs `grep_string` on whatever word the cursor is on. It's faster than typing `<Space>fw` and then re-entering the word.
--
-- **Workflow:** You see an unfamiliar function name. Put your cursor on it, press `<Space>fc`. Telescope shows every file that references it, with preview. No typing required.
--
-- ---
--
-- ### Telescope inside-picker controls
--
-- While a Telescope picker is open:
--
-- | Key | Action |
-- |-----|--------|
-- | `<C-q>` | Send all results to quickfix list |
-- | `<C-u>` / `<C-d>` | Scroll the preview pane |
-- | `<Tab>` | Multi-select an entry |
-- | `<C-x>` | Open in horizontal split |
-- | `<C-v>` | Open in vertical split |
-- | `<C-t>` | Open in new tab |
--
-- Multi-select + `<C-q>` is the key combo: select specific results with `<Tab>`, send just those to quickfix.
--
-- ---
--
-- ### `gd` → `<C-o>` — jump back from definition
--
-- `gd` takes you to a definition. `<C-o>` (jump list backward) takes you back. `<C-i>` goes forward again. Vim maintains a full jump history automatically.
--
-- | Key | Action |
-- |-----|--------|
-- | `<C-o>` | Jump back (previous location in jump list) |
-- | `<C-i>` | Jump forward |
-- | `''` | Toggle between last two jump positions |
--
-- **Workflow:** You're reading code and `gd` takes you three levels deep into a dependency. Press `<C-o>` repeatedly to unwind exactly back to where you started, preserving your position in each file.
--
-- ---
--
-- ### Text objects — operate on structure, not lines
--
-- Text objects work with `d`, `c`, `y`, `v` in normal/visual mode. They select semantic units.
--
-- | Command | Selects |
-- |---------|---------|
-- | `diw` | Delete inner word |
-- | `daw` | Delete word + surrounding space |
-- | `ci"` | Change inside quotes |
-- | `ca"` | Change quotes + contents |
-- | `ci(` | Change inside parentheses |
-- | `da{` | Delete block including braces |
-- | `dit` | Delete inside HTML/XML tag |
-- | `dap` | Delete a paragraph |
--
-- **Workflow:** You need to replace a function argument. `ci(` deletes everything between the parens and puts you in insert mode. `ca"` changes a string literal including its quotes. These are faster than any visual selection.
--
-- Treesitter (already installed) adds structural text objects — in Elixir, `daf` could delete an entire function. Add `nvim-treesitter-textobjects` to your plugins to unlock them.
--
-- ---
--
-- ### `*` and `#` — search for the word under cursor
--
-- `*` searches forward for the exact word under the cursor. `#` searches backward. No typing.
--
-- | Key | Action |
-- |-----|--------|
-- | `*` | Search forward for word under cursor |
-- | `#` | Search backward for word under cursor |
-- | `g*` | Same but matches partial words too |
-- | `n` / `N` | Next / previous match |
--
-- **Workflow:** You see a variable name and want to find all uses in the file. Put cursor on it, press `*`, then `n` to walk through every occurrence. Combined with `cgn` (change-next-match), this becomes a lightweight multi-rename: `*` to find, `cgn` to change the first, then `.` to repeat on each subsequent match.
--
-- ---
--
-- ### `.` (dot repeat) — the most underused key
--
-- `.` repeats the last change, including the text you typed. Combined with `n` (next search match), it's a surgical multi-file edit tool.
--
-- **Workflow:** You want to wrap a value in `to_string()` in several places. Make the change once. Press `n` to jump to the next occurrence. Press `.` to repeat the exact change. No macros needed for simple repetitive edits.
--
-- ---
--
-- ### `cgn` — change + repeat pattern
--
-- `cgn` changes the next match of the last search and leaves you positioned to press `.` for the next one.
--
-- 1. `*` on a word to set it as the search pattern
-- 2. `cgn` — change this match, type new text, press `<Esc>`
-- 3. `n.` — jump to next match, repeat the change
-- 4. `n.n.n.` — continue for as many as you want, skipping with `n` if needed
--
-- This is the vim equivalent of multi-cursor rename, but you control each change individually.
--
-- ---
--
-- ### `<Space>ls` — document symbols as a file outline
--
-- `<Space>ls` opens a Telescope picker with every function, module, and type in the current file. In a large Elixir module, this is a faster way to jump to a specific function than scrolling or searching.
--
-- **Workflow:** You're in a 500-line GenServer. Press `<Space>ls`, type `handle_call`, jump directly to the right clause.
--
-- ---
--
-- ### `<Space>lG` — workspace-wide symbol search
--
-- `<Space>lG` searches LSP symbols across the entire project. Type a function name, find where it's defined anywhere in the codebase without knowing which file it's in.
--
-- ---
--
-- ### Gitsigns hunk staging — commit partial changes
--
-- Instead of staging entire files, you can stage individual hunks:
--
-- | Key | Action |
-- |-----|--------|
-- | `]c` / `[c` | Jump between hunks |
-- | `<Space>ghp` | Preview hunk diff inline |
-- | `<Space>ghs` | Stage this hunk |
-- | `<Space>ghr` | Discard this hunk |
--
-- **Workflow:** You fixed a bug and also did some unrelated cleanup in the same file. Stage only the bug-fix hunks with `<Space>ghs`, leave the cleanup unstaged. Commit a clean, focused change.
--
-- ---
--
-- ### `<Space>gC` — blame by commit for current file
--
-- `<Space>gC` shows the commit history for the current file in Telescope. Select a commit to see the diff. `<Space>ghb` shows who last changed the line under the cursor with the full commit message.
--
-- **Workflow:** A line looks suspicious. `<Space>ghb` tells you who wrote it, when, and the commit message explaining why.
--
-- ---
--
-- ### Persistent undo — `undofile = true`
--
-- This config has `undofile = true`, so undo history survives closing and reopening files. You can undo changes from days ago.
--
-- **Workflow:** You closed a file last week after making a change. Open it again, press `u` — the full undo history is there.
--
-- ---
--
-- ### `<Space>fr` — paste from any register
--
-- Vim has many registers. `<Space>fr` opens a Telescope picker of all populated registers so you can browse and insert without memorizing what you yanked.
--
-- | Register | Contains |
-- |----------|----------|
-- | `"` | Default (last yank/delete) |
-- | `0` | Last explicit yank (not delete) |
-- | `+` | System clipboard |
-- | `1`–`9` | History of recent deletes |
-- | `a`–`z` | Named registers (set with `"ay`) |
--
-- **Tip:** `"0p` pastes the last yank even after a subsequent delete. This is the fix for accidentally overwriting your clipboard by deleting something.
--
-- ---
--
-- ### `%` — jump to matching bracket/delimiter
--
-- `%` jumps between matching `()`, `[]`, `{}`, `do`/`end` (with Treesitter). Press it on an opening brace to jump to the close, and back again.
--
-- **Workflow:** You're looking at a deeply nested `case` statement and want to find where it ends. Put cursor on `case`, press `%` to jump to `end`.
--
-- ---
--
-- ### `zz`, `zt`, `zb` — reframe the view without moving
--
-- | Key | Action |
-- |-----|--------|
-- | `zz` | Center current line in window |
-- | `zt` | Scroll so current line is at top |
-- | `zb` | Scroll so current line is at bottom |
--
-- **Workflow:** After `gd` drops you at a definition that lands at the bottom of the screen, press `zz` to center it without moving the cursor.
--
-- ---
--
-- ### `H`, `M`, `L` — jump within the visible screen
--
-- | Key | Action |
-- |-----|--------|
-- | `H` | Jump to top of visible screen |
-- | `M` | Jump to middle of visible screen |
-- | `L` | Jump to bottom of visible screen |
--
-- These move your cursor to the visible window area without scrolling. Similar to how Homerow/Vimium lets you click visible elements by label — `H`/`M`/`L` let you navigate to a visible area of code without searching.
--
-- ---
--
-- ### `<C-d>` / `<C-u>` — scroll half-page
--
-- Faster than holding `j`/`k`, and the cursor stays centered with `scrolloff = 8` already set. Use these to skim through long files.
--
-- ---
--
-- ### `gf` — open the file under the cursor
--
-- Put your cursor on a file path string (e.g. in an import or a comment) and press `gf` to open it. Works with relative paths.
--
-- ---
--
-- ### Spellcheck for writing
--
-- `<Space>us` toggles spellcheck. Useful when writing documentation, commit messages, or comments. With spell on:
--
-- | Key | Action |
-- |-----|--------|
-- | `]s` | Next misspelling |
-- | `[s` | Previous misspelling |
-- | `z=` | Show suggestions for word |
-- | `zg` | Add word to dictionary |
--
-- ─── Leaders (must be set before lazy) ───────────────────────────────────────
vim.g.mapleader = " "
vim.g.maplocalleader = ","
-- ─── Options ─────────────────────────────────────────────────────────────────
vim.opt.number = true
vim.opt.relativenumber = true
vim.opt.signcolumn = "yes"
vim.opt.wrap = false
vim.opt.spell = false
vim.opt.tabstop = 2
vim.opt.shiftwidth = 2
vim.opt.expandtab = true
vim.opt.smartindent = true
vim.opt.termguicolors = true
vim.opt.mouse = "a"
vim.opt.clipboard = "unnamedplus"
vim.opt.splitright = true
vim.opt.splitbelow = true
vim.opt.ignorecase = true
vim.opt.smartcase = true
vim.opt.undofile = true
vim.opt.scrolloff = 8
vim.opt.updatetime = 200
vim.opt.timeoutlen = 300 -- faster which-key popup
vim.opt.completeopt = { "menu", "menuone", "noselect" }
vim.opt.cursorline = true
vim.opt.showmode = false -- mode is shown in statusline
vim.opt.laststatus = 3 -- global statusline
-- ─── LSP status (used by lualine, must be defined before lazy setup) ─────────
do
local spinners = { "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" }
local frame = 0
local progress = {} -- [client_id] = spinner string
local ready = {} -- [client_id] = true when server responds to a real request
-- Spinner while LSP sends progress notifications
vim.api.nvim_create_autocmd("LspProgress", {
group = vim.api.nvim_create_augroup("lsp_progress", { clear = true }),
callback = function(ev)
local client_id = ev.data.client_id
local value = ev.data.params and ev.data.params.value
if value and value.kind == "end" then
progress[client_id] = nil
elseif value then
frame = (frame % #spinners) + 1
progress[client_id] = spinners[frame] .. " " .. (value.title or "")
end
vim.cmd("redrawstatus")
end,
})
-- After attach, poll with a cheap hover request until the server responds.
-- Only then mark it green.
vim.api.nvim_create_autocmd("LspAttach", {
group = vim.api.nvim_create_augroup("lsp_attach_status", { clear = true }),
callback = function(ev)
local client_id = ev.data.client_id
local bufnr = ev.buf
local client = vim.lsp.get_client_by_id(client_id)
if not client then return end
-- Poll every 2s until a hover request returns a non-error response
local timer = vim.uv.new_timer()
timer:start(2000, 2000, vim.schedule_wrap(function()
-- Stop if client is gone or buffer closed
if not vim.lsp.get_client_by_id(client_id) or not vim.api.nvim_buf_is_valid(bufnr) then
timer:stop()
timer:close()
return
end
-- Already confirmed ready
if ready[client_id] then
timer:stop()
timer:close()
return
end
-- Send a hover request; if we get any response (even empty) the server is up
local params = vim.lsp.util.make_position_params(0, client.offset_encoding)
client:request("textDocument/hover", params, function(err, result)
-- nil error = server responded (result may be nil for no hover, that's fine)
if err == nil then
ready[client_id] = true
timer:stop()
timer:close()
vim.cmd("redrawstatus")
end
end, bufnr)
end))
end,
})
vim.api.nvim_create_autocmd("LspDetach", {
group = vim.api.nvim_create_augroup("lsp_detach_status", { clear = true }),
callback = function(ev)
ready[ev.data.client_id] = nil
progress[ev.data.client_id] = nil
vim.cmd("redrawstatus")
end,
})
_G.lsp_status = function()
local clients = vim.lsp.get_clients({ bufnr = 0 })
if #clients == 0 then return "" end
local results = {}
for _, client in ipairs(clients) do
local msg = progress[client.id]
if msg then
table.insert(results, msg .. " [" .. client.name .. "]")
elseif not ready[client.id] then
frame = (frame % #spinners) + 1
table.insert(results, spinners[frame] .. " [" .. client.name .. "]")
else
table.insert(results, " " .. client.name)
end
end
return table.concat(results, " ")
end
_G.lsp_status_color = function()
local clients = vim.lsp.get_clients({ bufnr = 0 })
for _, client in ipairs(clients) do
if not ready[client.id] then
return { fg = "#f9e2af" } -- yellow: not ready
end
end
if #clients > 0 then
return { fg = "#a6e3a1" } -- green: all clients ready
end
return {}
end
end
-- ─── Bootstrap lazy.nvim ─────────────────────────────────────────────────────
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.uv.fs_stat(lazypath) then
vim.fn.system({
"git", "clone", "--filter=blob:none",
"https://github.com/folke/lazy.nvim.git",
"--branch=stable", lazypath,
})
end
vim.opt.rtp:prepend(lazypath)
-- ─── Plugins ─────────────────────────────────────────────────────────────────
require("lazy").setup({
-- ── Colorscheme ─────────────────────────────────────────────────────────
{
"AstroNvim/astrotheme",
priority = 1000,
opts = { style = "astrodark" },
config = function(_, opts)
require("astrotheme").setup(opts)
vim.cmd.colorscheme("astrodark")
end,
},
-- ── Which-key (keybinding discovery) ────────────────────────────────────
-- Press <Space> and wait to see available mappings
{
"folke/which-key.nvim",
event = "VeryLazy",
opts = { delay = 300 },
config = function(_, opts)
local wk = require("which-key")
wk.setup(opts)
-- Register group labels so the which-key menu has nice headers
wk.add({
{ "<leader>b", group = "Buffers" },
{ "<leader>f", group = "Find/Picker" },
{ "<leader>g", group = "Git" },
{ "<leader>gh", group = "Hunks" },
{ "<leader>l", group = "LSP" },
{ "<leader>m", group = "Mix (Elixir)" },
{ "<leader>p", group = "Packages/Plugins" },
{ "<leader>S", group = "Sessions" },
{ "<leader>t", group = "Terminal" },
{ "<leader>u", group = "UI Toggles" },
{ "<leader>x", group = "Lists" },
})
end,
},
-- ── Telescope (fuzzy finder) ─────────────────────────────────────────────
-- <Space>ff files | <Space>fw grep | <Space>fb buffers
{
"nvim-telescope/telescope.nvim",
cmd = "Telescope",
dependencies = {
"nvim-lua/plenary.nvim",
{ "nvim-telescope/telescope-fzf-native.nvim", build = "make" },
},
config = function(_, opts)
local actions = require("telescope.actions")
opts.defaults = vim.tbl_deep_extend("force", opts.defaults or {}, {
path_display = { "truncate" },
sorting_strategy = "ascending",
borderchars = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" },
layout_strategy = "horizontal",
layout_config = {
prompt_position = "top",
preview_width = 0.55,
width = 0.87,
height = 0.80,
},
results_title = false,
prompt_prefix = " ",
selection_caret = " ",
mappings = {
i = {
["<Esc>"] = actions.close,
["<C-k>"] = false, -- free for smart-splits (was preview_scrolling_right)
["<C-l>"] = false, -- free for smart-splits (was complete_tag)
},
n = {
["<C-k>"] = false, -- free for smart-splits
},
},
})
local telescope = require("telescope")
telescope.setup(opts)
pcall(telescope.load_extension, "fzf")
vim.api.nvim_set_hl(0, "TelescopePromptBorder", { fg = "#4fd6be" })
vim.api.nvim_set_hl(0, "TelescopeResultsBorder", { fg = "#4fd6be" })
vim.api.nvim_set_hl(0, "TelescopePreviewBorder", { fg = "#4fd6be" })
vim.api.nvim_set_hl(0, "TelescopePromptNormal", { bg = "NONE" })
end,
},
-- ── Neo-tree (file explorer) ─────────────────────────────────────────────
-- <Space>e toggle | <Space>o focus
{
"nvim-neo-tree/neo-tree.nvim",
branch = "v3.x",
cmd = "Neotree",
dependencies = { "nvim-lua/plenary.nvim", "nvim-tree/nvim-web-devicons", "MunifTanjim/nui.nvim" },
opts = {
filesystem = {
follow_current_file = { enabled = true },
hijack_netrw_behavior = "open_current",
},
},
},
-- ── Statusline (lualine, minimal) ────────────────────────────────────────
{
"nvim-lualine/lualine.nvim",
event = "VeryLazy",
dependencies = { "nvim-tree/nvim-web-devicons" },
opts = {
options = {
theme = "auto",
globalstatus = true,
component_separators = "|",
section_separators = "",
},
sections = {
lualine_a = { "mode" },
lualine_b = { "branch", "diff", "diagnostics" },
lualine_c = { { "filename", path = 1 } },
lualine_x = {
{ _G.lsp_status, color = _G.lsp_status_color },
"filetype",
},
lualine_y = { "progress" },
lualine_z = { "location" },
},
},
},
-- ── Bufferline (tabs for buffers) ────────────────────────────────────────
-- ]b / [b next/prev | <Space>bb pick buffer
{
"akinsho/bufferline.nvim",
event = "VeryLazy",
dependencies = "nvim-tree/nvim-web-devicons",
opts = {
options = {
diagnostics = "nvim_lsp",
always_show_bufferline = false,
offsets = {
{ filetype = "neo-tree", text = "Explorer", highlight = "Directory", text_align = "left" },
},
},
},
},
-- ── Treesitter (parser installer) ───────────────────────────────────────
-- NOTE: In nvim-treesitter v1.0+, highlighting is built into Neovim itself.
-- This plugin is now only a parser installer. Highlighting is enabled below
-- via an autocommand that calls vim.treesitter.start().
{
"nvim-treesitter/nvim-treesitter",
build = ":TSUpdate",
event = { "BufReadPost", "BufNewFile", "VeryLazy" },
config = function()
-- v1.0 setup only accepts install_dir, nothing else
require("nvim-treesitter").setup()
end,
},
-- ── Mason (LSP/tool installer) ───────────────────────────────────────────
-- :Mason open UI | :MasonInstall <name> install a tool
{
"williamboman/mason.nvim",
cmd = "Mason",
opts = {},
},
{
"williamboman/mason-lspconfig.nvim",
dependencies = { "williamboman/mason.nvim" },
opts = {
-- Add LSP servers to auto-install here. Examples:
-- "lua_ls", "pyright", "ts_ls", "rust_analyzer"
ensure_installed = { "lua_ls" },
automatic_installation = false,
},
},
-- ── LSP ──────────────────────────────────────────────────────────────────
-- K hover docs
-- gd go to definition | grr references | grn rename
-- gra code actions | gD declaration
-- <Space>lf format | <Space>ld line diagnostics
{
"neovim/nvim-lspconfig",
event = { "BufReadPost", "BufNewFile" },
dependencies = { "williamboman/mason-lspconfig.nvim" },
config = function()
-- Keymaps and autoformat wired up on every LSP attach
vim.api.nvim_create_autocmd("LspAttach", {
group = vim.api.nvim_create_augroup("lsp_attach_keymaps", { clear = true }),
callback = function(ev)
local bufnr = ev.buf
local client = vim.lsp.get_client_by_id(ev.data.client_id)
local map = function(keys, func, desc)
vim.keymap.set("n", keys, func, { buffer = bufnr, desc = "LSP: " .. desc })
end
map("K", vim.lsp.buf.hover, "Hover docs")
map("gd", vim.lsp.buf.definition, "Go to definition")
map("gD", vim.lsp.buf.declaration, "Go to declaration")
map("gy", vim.lsp.buf.type_definition, "Go to type definition")
map("gri", vim.lsp.buf.implementation, "Go to implementation")
map("grr", vim.lsp.buf.references, "References")
map("grn", vim.lsp.buf.rename, "Rename symbol")
map("gra", vim.lsp.buf.code_action, "Code action")
map("<Leader>lf", function() vim.lsp.buf.format({ async = true }) end, "Format document")
map("<Leader>lr", vim.lsp.buf.rename, "Rename symbol")
map("<Leader>la", vim.lsp.buf.code_action, "Code action")
map("<Leader>lh", vim.lsp.buf.signature_help, "Signature help")
map("<Leader>li", "<Cmd>LspInfo<CR>", "LSP info")
map("<Leader>ld", vim.diagnostic.open_float, "Line diagnostics")
map("gl", vim.diagnostic.open_float, "Line diagnostics")
map("]d", function() vim.diagnostic.jump({ count = 1 }) end, "Next diagnostic")
map("[d", function() vim.diagnostic.jump({ count = -1 }) end, "Prev diagnostic")
-- Auto-format on save (toggle with <Space>uf / <Space>uF)
if client and client.supports_method("textDocument/formatting") then
vim.api.nvim_create_autocmd("BufWritePre", {
buffer = bufnr,
callback = function()
if vim.b[bufnr].autoformat ~= false and vim.g.autoformat ~= false then
vim.lsp.buf.format({ bufnr = bufnr, async = false })
end
end,
})
end
end,
})
-- Default capabilities (enhanced by blink.cmp if present)
local capabilities = vim.lsp.protocol.make_client_capabilities()
local ok, blink = pcall(require, "blink.cmp")
if ok then capabilities = blink.get_lsp_capabilities(capabilities) end
-- ── Configure servers via the new vim.lsp.config API (nvim 0.11+) ──
-- lua_ls (managed by mason)
vim.lsp.config("lua_ls", {
capabilities = capabilities,
settings = {
Lua = {
runtime = { version = "LuaJIT" },
diagnostics = { globals = { "vim" } },
workspace = { library = vim.api.nvim_get_runtime_file("", true), checkThirdParty = false },
telemetry = { enable = false },
},
},
})
vim.lsp.enable("lua_ls")
-- ── Elixir: "expert" LSP (your custom binary) ─────────────────────
-- To switch to lexical, change cmd to:
-- { "/Users/alex/Code/lexical/_build/dev/package/lexical/bin/start_lexical.sh" }
vim.lsp.config("expert", {
cmd = { "/usr/local/bin/expert", "--stdio" },
filetypes = { "elixir", "eelixir", "heex" },
root_markers = { "mix.exs", ".git" },
capabilities = capabilities,
})
vim.lsp.enable("expert")
-- ── Add more servers here as needed ───────────────────────────────
-- vim.lsp.config("pyright", { capabilities = capabilities })
-- vim.lsp.enable("pyright")
-- vim.lsp.config("ts_ls", { capabilities = capabilities })
-- vim.lsp.enable("ts_ls")
end,
},
-- ── Blink.cmp (autocompletion) ───────────────────────────────────────────
-- <C-Space> open menu | <CR> confirm | <C-e> cancel
-- <Tab>/<S-Tab> next/prev | <C-d>/<C-u> scroll docs
{
"saghen/blink.cmp",
version = "*",
event = "InsertEnter",
dependencies = { "L3MON4D3/LuaSnip" },
opts = {
keymap = {
preset = "default",
["<CR>"] = { "accept", "fallback" },
["<Tab>"] = { "snippet_forward", "select_next", "fallback" },
["<S-Tab>"] = { "snippet_backward", "select_prev", "fallback" },
["<C-e>"] = { "cancel", "fallback" },
["<C-u>"] = { "scroll_documentation_up", "fallback" },
["<C-d>"] = { "scroll_documentation_down", "fallback" },
},
sources = {
default = { "lsp", "path", "snippets", "buffer" },
},
completion = {
documentation = { auto_show = true },
},
snippets = { preset = "luasnip" },
},
},
-- ── LuaSnip (snippets engine for blink.cmp) ──────────────────────────────
{
"L3MON4D3/LuaSnip",
version = "v2.*",
dependencies = { "rafamadriz/friendly-snippets" },
config = function()
require("luasnip.loaders.from_vscode").lazy_load()
end,
},
-- ── Gitsigns (git decorations) ───────────────────────────────────────────
-- ]c / [c next/prev hunk | <Space>gh* git hunk operations
{
"lewis6991/gitsigns.nvim",
event = { "BufReadPost", "BufNewFile" },
opts = {
signs = {
add = { text = "▎" },
change = { text = "▎" },
delete = { text = "" },
topdelete = { text = "" },
changedelete = { text = "▎" },
},
on_attach = function(bufnr)
local gs = require("gitsigns")
local map = function(keys, func, desc)
vim.keymap.set("n", keys, func, { buffer = bufnr, desc = "Git: " .. desc })
end
map("]c", function() gs.nav_hunk("next") end, "Next hunk")
map("[c", function() gs.nav_hunk("prev") end, "Prev hunk")
map("<Leader>ghs", gs.stage_hunk, "Stage hunk")
map("<Leader>ghr", gs.reset_hunk, "Reset hunk")
map("<Leader>ghS", gs.stage_buffer, "Stage buffer")
map("<Leader>ghR", gs.reset_buffer, "Reset buffer")
map("<Leader>ghp", gs.preview_hunk, "Preview hunk")
map("<Leader>ghb", function() gs.blame_line({ full = true }) end, "Blame line")
map("<Leader>ghd", gs.diffthis, "Diff this")
end,
},
},
-- ── Toggleterm (floating/split terminals) ────────────────────────────────
-- <M-`> toggle floating terminal
-- <Space>tf floating | <Space>th horizontal | <Space>tv vertical
-- <Space>tl lazygit
{
"akinsho/toggleterm.nvim",
version = "*",
cmd = "ToggleTerm",
keys = { "<M-`>" },
opts = {
open_mapping = [[<M-`>]],
direction = "float",
size = function(term)
if term.direction == "horizontal" then
return 15
elseif term.direction == "vertical" then
return math.floor(vim.o.columns * 0.4)
end
end,
float_opts = { border = "rounded" },
on_create = function()
vim.opt_local.foldcolumn = "0"
vim.opt_local.signcolumn = "no"
end,
},
},
-- ── Autopairs ────────────────────────────────────────────────────────────
{
"windwp/nvim-autopairs",
event = "InsertEnter",
opts = { check_ts = true },
},
-- ── Better escape (jj / jk → <Esc>) ─────────────────────────────────────
{
"max397574/better-escape.nvim",
event = "InsertEnter",
opts = { default_mappings = true },
},
-- ── Comments (gcc / gbc / <Space>/) ─────────────────────────────────────
{
"numToStr/Comment.nvim",
keys = {
{ "gcc", mode = "n" },
{ "gc", mode = { "n", "v" } },
{ "gbc", mode = "n" },
{ "gb", mode = { "n", "v" } },
},
opts = {},
},
-- ── Todo comments (highlights TODO/FIXME/HACK etc.) ──────────────────────
{
"folke/todo-comments.nvim",
dependencies = "nvim-lua/plenary.nvim",
event = { "BufReadPost", "BufNewFile" },
opts = {},
},
-- ── vim-illuminate (highlight word under cursor) ─────────────────────────
{
"RRethy/vim-illuminate",
event = { "BufReadPost", "BufNewFile" },
opts = { delay = 200 },
config = function(_, opts)
require("illuminate").configure(opts)
end,
},
-- ── Indent guides ────────────────────────────────────────────────────────
{
"lukas-reineke/indent-blankline.nvim",
event = { "BufReadPost", "BufNewFile" },
main = "ibl",
opts = {
indent = { char = "│" },
scope = { enabled = true, show_start = false, show_end = false },
},
},
-- ── Nvim-web-devicons (file type icons, requires Nerd Font) ──────────────
{ "nvim-tree/nvim-web-devicons", lazy = true },
-- ── Plenary (utility library used by telescope etc.) ─────────────────────
{ "nvim-lua/plenary.nvim", lazy = true },
-- ── Smart splits (window resizing + tmux compat) ─────────────────────────
-- <C-h/j/k/l> navigate | <leader-arrows> resize
{
"mrjones2014/smart-splits.nvim",
event = "VeryLazy",
config = function()
local ss = require("smart-splits")
vim.keymap.set("n", "<leader><Up>", ss.resize_up, { desc = "Resize window up" })
vim.keymap.set("n", "<leader><Down>", ss.resize_down, { desc = "Resize window down" })
vim.keymap.set("n", "<leader><Left>", ss.resize_left, { desc = "Resize window left" })
vim.keymap.set("n", "<leader><Right>", ss.resize_right, { desc = "Resize window right" })
vim.keymap.set("n", "<C-h>", ss.move_cursor_left, { desc = "Move to left window" })
vim.keymap.set("n", "<C-j>", ss.move_cursor_down, { desc = "Move to below window" })
vim.keymap.set("n", "<C-k>", ss.move_cursor_up, { desc = "Move to above window" })
vim.keymap.set("n", "<C-l>", ss.move_cursor_right, { desc = "Move to right window" })
end,
},
-- ── Dashboard (home screen) ───────────────────────────────────────────────
-- <Space>h open dashboard
{
"nvimdev/dashboard-nvim",
event = "VimEnter",
dependencies = { "nvim-tree/nvim-web-devicons" },
opts = {
theme = "doom",
config = {
header = {
"",
"",
"",
" █████╗ ██╗ ███████╗██╗ ██╗ █████╗ ",
"██╔══██╗██║ ██╔════╝╚██╗██╔╝██╔══██╗",
"███████║██║ █████╗ ╚███╔╝ ███████║",
"██╔══██║██║ ██╔══╝ ██╔██╗ ██╔══██║",
"██║ ██║███████╗███████╗██╔╝ ██╗██║ ██║",
"╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝",
"",
"███╗ ██╗██╗ ██╗██╗███╗ ███╗",
"████╗ ██║██║ ██║██║████╗ ████║",
"██╔██╗ ██║██║ ██║██║██╔████╔██║",
"██║╚██╗██║╚██╗ ██╔╝██║██║╚██╔╝██║",
"██║ ╚████║ ╚████╔╝ ██║██║ ╚═╝ ██║",
"╚═╝ ╚═══╝ ╚═══╝ ╚═╝╚═╝ ╚═╝",
"",
"",
},
center = {
{ icon = " ", key = "f", desc = "Find File", action = "Telescope find_files" },
{ icon = " ", key = "o", desc = "Old Files", action = "Telescope oldfiles" },
{ icon = " ", key = "w", desc = "Live Grep", action = "Telescope live_grep" },
{ icon = " ", key = "e", desc = "File Explorer", action = "Neotree toggle" },
{ icon = " ", key = "q", desc = "Quit", action = "qa" },
},
footer = {},
},
},
},
-- ── Persistence (session management) ─────────────────────────────────────
-- <Space>Ss save | <Space>Sl load last | <Space>S. load cwd
{
"folke/persistence.nvim",
event = "BufReadPre",
opts = {},
},
}, {
-- lazy.nvim global options
install = { colorscheme = { "astrodark", "habamax" } },
performance = {
rtp = {
disabled_plugins = { "gzip", "netrwPlugin", "tar", "tarPlugin", "zip", "zipPlugin", "tohtml" },
},
},
})
-- =============================================================================
-- Keymaps
-- =============================================================================
local map = function(mode, lhs, rhs, opts)
opts = opts or {}
opts.silent = opts.silent ~= false
vim.keymap.set(mode, lhs, rhs, opts)
end
-- ── Save / Quit ───────────────────────────────────────────────────────────
map("n", "<Leader>w", "<Cmd>w<CR>", { desc = "Save file" })
-- ── Smart close (<Space>q) ────────────────────────────────────────────────
-- Multiple windows → close the current window
-- Single window → delete the current buffer (keeps the window open)
map("n", "<Leader>q", function()
if #vim.api.nvim_tabpage_list_wins(0) > 1 then
vim.cmd("close")
else
local bufs = vim.fn.getbufinfo({ buflisted = 1 })
if #bufs <= 1 then
vim.cmd("quit")
else
local buf = vim.api.nvim_get_current_buf()
local ok = pcall(vim.cmd, "bprevious")
if not ok then pcall(vim.cmd, "bnext") end
vim.api.nvim_buf_delete(buf, { force = false })
end
end
end, { desc = "Close window or buffer" })
-- ── Splits ────────────────────────────────────────────────────────────────
map("n", "|", "<Cmd>vsplit<CR>", { desc = "Vertical split" })
map("n", "\\", "<Cmd>split<CR>", { desc = "Horizontal split" })
-- ── New file ──────────────────────────────────────────────────────────────
map("n", "<Leader>n", "<Cmd>enew<CR>", { desc = "New file" })
-- ── Rename current file ───────────────────────────────────────────────────
map("n", "<Leader>R", function()
local old = vim.fn.expand("%")
local new = vim.fn.input("Rename to: ", old)
if new ~= "" and new ~= old then
vim.cmd("saveas " .. new)
vim.fn.delete(old)
vim.cmd("bdelete #")
end
end, { desc = "Rename current file" })
-- ── Buffer navigation ─────────────────────────────────────────────────────
map("n", "]b", "<Cmd>bnext<CR>", { desc = "Next buffer" })
map("n", "[b", "<Cmd>bprevious<CR>", { desc = "Prev buffer" })
map("n", "<Leader>c", function()
local buf = vim.api.nvim_get_current_buf()
local ok = pcall(vim.cmd, "bprevious")
if not ok then pcall(vim.cmd, "bnext") end
vim.api.nvim_buf_delete(buf, { force = false })
end, { desc = "Close buffer" })
map("n", "<Leader>bb", "<Cmd>Telescope buffers<CR>", { desc = "Pick buffer" })
map("n", "<Leader>bc", "<Cmd>%bd|e#|bd#<CR>", { desc = "Close all other buffers" })
-- ── Commenting ────────────────────────────────────────────────────────────
map("n", "<Leader>/", "gcc", { desc = "Toggle comment", remap = true })
map("v", "<Leader>/", "gc", { desc = "Toggle comment", remap = true })
-- ── Dashboard ─────────────────────────────────────────────────────────────
map("n", "<Leader>h", "<Cmd>Dashboard<CR>", { desc = "Home (dashboard)" })
-- ── File explorer ─────────────────────────────────────────────────────────
map("n", "<Leader>e", "<Cmd>Neotree toggle<CR>", { desc = "Toggle file explorer" })
map("n", "<Leader>o", "<Cmd>Neotree focus<CR>", { desc = "Focus file explorer" })
-- ── Telescope / Finder ────────────────────────────────────────────────────
map("n", "<Leader>ff", "<Cmd>Telescope find_files<CR>", { desc = "Find files" })
map("n", "<Leader>fF", "<Cmd>Telescope find_files hidden=true<CR>", { desc = "Find files (hidden)" })
map("n", "<Leader>fw", "<Cmd>Telescope live_grep<CR>", { desc = "Live grep" })
map("n", "<Leader>fW", "<Cmd>Telescope live_grep additional_args={'--hidden'}<CR>", { desc = "Live grep (hidden)" })
map("n", "<Leader>fb", "<Cmd>Telescope buffers<CR>", { desc = "Buffers" })
map("n", "<Leader>fo", "<Cmd>Telescope oldfiles<CR>", { desc = "Old files" })
map("n", "<Leader>fc", "<Cmd>Telescope grep_string<CR>", { desc = "Word at cursor" })
map("n", "<Leader>fC", "<Cmd>Telescope commands<CR>", { desc = "Commands" })
map("n", "<Leader>fh", "<Cmd>Telescope help_tags<CR>", { desc = "Help tags" })
map("n", "<Leader>fk", "<Cmd>Telescope keymaps<CR>", { desc = "Keymaps" })
map("n", "<Leader>fm", "<Cmd>Telescope man_pages<CR>", { desc = "Man pages" })
map("n", "<Leader>fr", "<Cmd>Telescope registers<CR>", { desc = "Registers" })
map("n", "<Leader>ft", "<Cmd>Telescope colorscheme<CR>", { desc = "Colorschemes" })
map("n", "<Leader>f'", "<Cmd>Telescope marks<CR>", { desc = "Marks" })
map("n", "<Leader>gb", "<Cmd>Telescope git_branches<CR>", { desc = "Git branches" })
map("n", "<Leader>gc", "<Cmd>Telescope git_commits<CR>", { desc = "Git commits" })
map("n", "<Leader>gC", "<Cmd>Telescope git_bcommits<CR>", { desc = "Git commits (file)" })
map("n", "<Leader>gt", "<Cmd>Telescope git_status<CR>", { desc = "Git status" })
-- ── Terminal ──────────────────────────────────────────────────────────────
-- Helper: open a one-off floating terminal with a given command
local function float_term(cmd)
return function()
local Terminal = require("toggleterm.terminal").Terminal
Terminal:new({ cmd = cmd, direction = "float", close_on_exit = true }):toggle()
end
end
map("n", "<Leader>tf", "<Cmd>ToggleTerm direction=float<CR>", { desc = "Float terminal" })
map("n", "<Leader>th", "<Cmd>ToggleTerm direction=horizontal<CR>", { desc = "Horizontal terminal" })
map("n", "<Leader>tv", "<Cmd>ToggleTerm direction=vertical<CR>", { desc = "Vertical terminal" })
-- Lazygit (<Space>gg or <Space>tl) — reuses the same terminal instance
local lazygit_term = nil
local function toggle_lazygit()
local Terminal = require("toggleterm.terminal").Terminal
if not lazygit_term then
lazygit_term = Terminal:new({ cmd = "lazygit", direction = "float", close_on_exit = true })
end
lazygit_term:toggle()
end
map("n", "<Leader>gg", toggle_lazygit, { desc = "Lazygit" })
map("n", "<Leader>tl", toggle_lazygit, { desc = "ToggleTerm lazygit" })
-- Claude AI terminal (<Space>tc)
map("n", "<Leader>tc",
float_term("claude"),
{ desc = "ToggleTerm claude" })
-- ── Elixir / Mix ──────────────────────────────────────────────────────────
map("n", "<Leader>ms",
float_term("mix test --stale; exec $SHELL"),
{ desc = "mix test --stale" })
map("n", "<Leader>mf",
float_term("mix test --failed; exec $SHELL"),
{ desc = "mix test --failed" })
map("n", "<Leader>mt", function()
local file = vim.fn.expand("%")
local line = vim.fn.line(".")
float_term("mix test " .. file .. ":" .. line .. "; exec $SHELL")()
end, { desc = "mix test (line)" })
map("n", "<Leader>mT", function()
local file = vim.fn.expand("%")
float_term("mix test " .. file .. "; exec $SHELL")()
end, { desc = "mix test (file)" })
map("n", "<Leader>mq", function()
vim.fn.setqflist({}, " ", {
title = "mix test",
lines = vim.fn.systemlist("mix test --failed 2>&1"),
efm = table.concat({
"%E %n) %m",
"%C %f:%l",
"%C %m",
"%-G%.%#",
}, ","),
})
vim.cmd("copen")
end, { desc = "mix test --failed → quickfix" })
-- ── Session management ────────────────────────────────────────────────────
map("n", "<Leader>Ss", function() require("persistence").save() end, { desc = "Save session" })
map("n", "<Leader>Sl", function() require("persistence").load({ last = true }) end, { desc = "Load last session" })
map("n", "<Leader>S.", function() require("persistence").load() end, { desc = "Load cwd session" })
map("n", "<Leader>Sd", function() require("persistence").stop() end, { desc = "Stop session persistence" })
-- ── Package management ────────────────────────────────────────────────────
map("n", "<Leader>pm", "<Cmd>Mason<CR>", { desc = "Open Mason" })
map("n", "<Leader>ps", "<Cmd>Lazy<CR>", { desc = "Plugin status" })
map("n", "<Leader>pS", "<Cmd>Lazy sync<CR>", { desc = "Plugins sync" })
map("n", "<Leader>pu", "<Cmd>Lazy check<CR>", { desc = "Check plugin updates" })
map("n", "<Leader>pU", "<Cmd>Lazy update<CR>", { desc = "Update plugins" })
-- ── Quickfix / Location lists ─────────────────────────────────────────────
map("n", "<Leader>xq", "<Cmd>copen<CR>", { desc = "Open quickfix" })
map("n", "<Leader>xl", "<Cmd>lopen<CR>", { desc = "Open location list" })
map("n", "]q", "<Cmd>cnext<CR>", { desc = "Next quickfix" })
map("n", "[q", "<Cmd>cprevious<CR>", { desc = "Prev quickfix" })
map("n", "]l", "<Cmd>lnext<CR>", { desc = "Next location" })
map("n", "[l", "<Cmd>lprevious<CR>", { desc = "Prev location" })
-- ── UI Toggles (<Space>u*) ────────────────────────────────────────────────
map("n", "<Leader>un", function()
vim.opt.relativenumber = not vim.opt.relativenumber:get()
vim.opt.number = not vim.opt.number:get()
end, { desc = "Toggle line numbers" })
map("n", "<Leader>uw", function()
vim.opt.wrap = not vim.opt.wrap:get()
end, { desc = "Toggle wrap" })
map("n", "<Leader>us", function()
vim.opt.spell = not vim.opt.spell:get()
end, { desc = "Toggle spellcheck" })
map("n", "<Leader>ud", function()
local enabled = vim.diagnostic.is_enabled and vim.diagnostic.is_enabled()
if enabled == nil then enabled = true end
vim.diagnostic.enable(not enabled)
end, { desc = "Toggle diagnostics" })
map("n", "<Leader>uf", function()
vim.b.autoformat = not (vim.b.autoformat == false)
vim.notify("Autoformat " .. (vim.b.autoformat == false and "disabled" or "enabled") .. " (buffer)")
end, { desc = "Toggle autoformat (buffer)" })
map("n", "<Leader>uF", function()
vim.g.autoformat = not (vim.g.autoformat == false)
vim.notify("Autoformat " .. (vim.g.autoformat == false and "disabled" or "enabled") .. " (global)")
end, { desc = "Toggle autoformat (global)" })
map("n", "<Leader>uh", function()
vim.lsp.inlay_hint.enable(not vim.lsp.inlay_hint.is_enabled())
end, { desc = "Toggle inlay hints" })
map("n", "<Leader>ub", function()
vim.opt.background = vim.opt.background:get() == "dark" and "light" or "dark"
end, { desc = "Toggle background" })
-- ── LSP via Telescope ─────────────────────────────────────────────────────
map("n", "<Leader>ls", "<Cmd>Telescope lsp_document_symbols<CR>", { desc = "Document symbols" })
map("n", "<Leader>lG", "<Cmd>Telescope lsp_workspace_symbols<CR>", { desc = "Workspace symbols" })
map("n", "<Leader>lD", "<Cmd>Telescope diagnostics<CR>", { desc = "All diagnostics" })
map("n", "<Leader>lR", "<Cmd>Telescope lsp_references<CR>", { desc = "References" })
-- ── Config editing ────────────────────────────────────────────────────────
map("n", "<Leader>ev", "<Cmd>e $MYVIMRC<CR>", { desc = "Edit init.lua" })
map("n", "<Leader>sv", "<Cmd>source $MYVIMRC<CR>", { desc = "Source init.lua" })
-- ── Misc ──────────────────────────────────────────────────────────────────
-- Clear search highlight on <Esc>
map("n", "<Esc>", "<Cmd>nohlsearch<CR>", { desc = "Clear search highlight" })
-- Move selected lines up/down in visual mode
map("v", "J", ":m '>+1<CR>gv=gv", { desc = "Move selection down" })
map("v", "K", ":m '<-2<CR>gv=gv", { desc = "Move selection up" })
-- =============================================================================
-- Autocommands
-- =============================================================================
-- Enable treesitter highlighting (native nvim API, nvim-treesitter v1.0+)
vim.api.nvim_create_autocmd("FileType", {
group = vim.api.nvim_create_augroup("treesitter_highlight", { clear = true }),
callback = function()
pcall(vim.treesitter.start)
end,
})
-- Highlight yanked text briefly
vim.api.nvim_create_autocmd("TextYankPost", {
group = vim.api.nvim_create_augroup("highlight_yank", { clear = true }),
callback = function() vim.highlight.on_yank() end,
})
-- Restore cursor to last position when reopening a file
vim.api.nvim_create_autocmd("BufReadPost", {
group = vim.api.nvim_create_augroup("restore_cursor", { clear = true }),
callback = function()
local row, col = unpack(vim.api.nvim_buf_get_mark(0, '"'))
if row > 0 and row <= vim.api.nvim_buf_line_count(0) then
vim.api.nvim_win_set_cursor(0, { row, col })
end
end,
})
-- Resize splits automatically when the terminal window is resized
vim.api.nvim_create_autocmd("VimResized", {
group = vim.api.nvim_create_augroup("auto_resize", { clear = true }),
callback = function() vim.cmd("tabdo wincmd =") end,
})
-- Close certain utility windows with just 'q'
vim.api.nvim_create_autocmd("FileType", {
group = vim.api.nvim_create_augroup("close_with_q", { clear = true }),
pattern = { "help", "lspinfo", "man", "qf", "checkhealth", "lazy", "mason", "dashboard" },
callback = function(ev)
vim.keymap.set("n", "q", "<Cmd>close<CR>", { buffer = ev.buf, silent = true })
end,
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment