Created
February 24, 2026 19:37
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -- ============================================================================= | |
| -- 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-`>` | 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