A personal, batteries-included Neovim setup in pure Lua. Built around two daily drivers — Java / Spring Boot and the web stack (TypeScript, Angular, HTML/CSS) — plus an Obsidian-style Markdown workflow.
Managed by lazy.nvim, themed with Catppuccin Mocha, tested with headless specs.
- Java (Spring Boot) —
nvim-jdtlswith Lombok agent,java-debug-adapterandjava-testbundles wired straight into jdtls, JUnit run/test keymaps. - New-file templates —
BufNewFileskeletons for Java (packagefromsrc/main|test/java+public class <Name>, plusmainfor standalone files), C, C++, headers, C#, Go, Kotlin, Rust and PHP; existing files are never touched. - Markdown / Obsidian — a hand-written editing core: emphasis toggles,
links, wikilinks, headings, checkboxes, quotes, and list continuation with
automatic ordered-list renumbering. Rendered in-buffer by
render-markdown.nvim. - Web frontend indentation — a custom
indentexpr(VSCodeonEnterRuleslogic) replaces Treesitter indent for HTML/Angular/JS/TS/JSX so<div>,{,(and[break exactly the way you expect. - LSP everywhere — installs and configures 20+ servers via Mason: jdtls, vtsls, pyright, gopls, rust-analyzer, clangd, angular, kotlin, html/css/json/eslint/tailwind/yaml/bash/dockerfile, marksman and more.
- Inline AI —
opencode.nvimin a right-side pane, context-aware (current file, selection, diagnostics). - UI polish — Catppuccin Mocha, Lualine, Bufferline (pinned buffers), Neo-tree, Telescope, Noice, indent-blankline.
- Headless test suite — plain
nvim --headlessspecs, no framework needed.
| Requirement | Notes |
|---|---|
| Neovim ≥ 0.11 | Uses vim.lsp.config, vim.treesitter.start, and the main branch of nvim-treesitter. |
| git, ripgrep | Plugin bootstrap and Telescope live-grep. |
| JDK 17/21 | jdtls runtime is hardcoded to /usr/lib/jvm/java-21-openjdk (see Notes). |
| Maven or Gradle | For Java project roots (pom.xml, mvnw, gradlew, .git). |
| Nerd Font | JetBrainsMono Nerd Font is the GUI font. |
C compiler + tree-sitter CLI |
Building Treesitter parsers. |
| Alacritty (optional) | Used by the Neovide <C-S-n> keymap. |
latex2text (optional) |
LaTeX conversion in render-markdown.nvim. |
Back up anything already in
~/.config/nvimfirst.
git clone git@github.com:AliFuatAkyemis/Neovim-Config.git ~/.config/nvim
nvimOn first launch lazy.nvim bootstraps itself, then installs every plugin:
- Wait for Lazy to finish —
:Lazyto inspect. :Mason— verifyjdtls,java-debug-adapter,java-testand the other servers are installed.:TSUpdate— builds the Treesitter parsers listed inlua/plugins/treesitter.lua.
lazy-lock.json is gitignored on purpose, so plugins track their latest
compatible revisions after a fresh clone.
Leader is Space; local leader is \.
| Key | Action |
|---|---|
<C-p> |
Find files (Telescope) |
<leader>fg |
Live grep |
<C-n> |
Toggle Neo-tree |
<leader>j |
Neo-tree window: expand/collapse focused node |
<C-j> |
Terminal: opens below the text window (or the current window when no text window is open) in the startup directory; if it is open but unfocused, focuses it; if it is focused, closes it and returns to the window it was entered from (Neo-tree, opencode, or text). <Esc> leaves Terminal mode for normal-mode scrolling (hjkl, gg/G, Ctrl-u/d, visual selection); i/I/a/A return to Terminal mode. In normal mode <Esc> is forwarded to the running program (vim, less, fzf) |
<leader>w |
Save file |
<leader>f |
Format buffer (conform) |
<leader>? |
Buffer-local keymaps (which-key) |
<C-k> |
Floating diagnostic |
<M-CR> |
Add the missing import for the underlined symbol (normal / insert, no menu; picks the module already imported in the file, else the first candidate) |
<C-M-CR> |
All LSP code actions in a Telescope picker (normal / insert; insert leaves insert mode so the prompt is usable) |
<leader>r |
Clear search highlight + redraw |
<leader>up |
Toggle LSP progress |
Telescope prompt input uses readline-style editing: <C-u> delete to start,
<C-w> delete word, <C-k> delete to end, <C-e> end of line, <C-b> /
<C-f> one char left / right, <C-d> delete forward. <Esc> closes the
picker on the first press.
| Key | Action |
|---|---|
L / H |
Next / previous buffer |
<leader>x |
Close buffer, keep the split layout |
<leader>bp |
Pin / unpin buffer (Bufferline) |
| Key | Action |
|---|---|
<C-l>, <C-Right> |
Next word — normal/insert stop at the word end before wrapping; visual uses native <C-Right> |
<C-h>, <C-Left> |
Previous word — normal, insert and all visual modes (visual, visual-line, visual-block) |
<C-a> |
Start of line in insert mode (also Neo-tree create/rename/move inputs) |
<C-BS> |
Delete previous word (insert) |
<C-H>, <C-BS> |
Delete previous word (cmdline) |
<C-Del> |
Delete next word |
Tab / S-Tab (insert) |
Completion / snippet navigation |
<C-h> / <C-l> mirror Ctrl+← / Ctrl+→. Insert-mode
<C-h> no longer deletes; word deletion there is <C-BS> (requires a terminal
with Kitty Keyboard Protocol support — Alacritty 0.13+).
| Key | Action |
|---|---|
y / p |
Neovim registers only — does not touch the system clipboard |
<leader>y |
Yank line/selection to the system clipboard |
<leader>p / <leader>P |
Paste from clipboard (after / before) |
<C-S-c> |
Copy selection to clipboard (visual) |
<C-S-v> |
Paste from clipboard (normal / visual / insert / cmd) |
One key everywhere: gc.
| Mode | Action |
|---|---|
| Normal | Toggle // on the current line |
| Visual line, 1 line | Toggle // |
| Visual line, 2+ lines | Formatter-style /* + * block |
| Visual block | Wrap only the selected part of each line in /* ... */ |
Blocks are uncommented even after the formatter rewrites them (* prefixes,
spacing or tab variations). gcc, gb, gco and the other Comment.nvim
mappings are removed; gc is the only comment key.
Active in markdown buffers only.
| Key | Action |
|---|---|
<C-b> |
Bold (normal / visual / insert) |
<C-i>, Tab |
Italic |
<C-e> |
Inline code |
<C-s> |
|
<leader>lh |
==Highlight== |
<C-k> |
Insert link [text](url) |
<leader>l |
Insert wikilink [[Note]] |
<C-1> … <C-6> |
Heading level 1–6 (press again to clear) |
<C-Enter> |
Toggle checkbox - [ ] ↔ - [x] |
<Enter> |
Continue list / next ordered number |
<Shift-Enter> |
Soft break, aligned to the item's content column |
o |
New item below |
<leader>q |
Toggle block quote |
Tab / S-Tab (visual) |
Indent / outdent |
Behavioral details:
- Emphasis toggles the whole span the cursor sits in, and handles
***bold italic***style combinations without corrupting them. - Ordering is renumbered on edits and on insert-leave, skipping fenced code blocks.
- Normal-mode
<Enter>only toggles existing checkboxes; it never converts a plain list item into one.
Buffer-local, loaded from ftplugin/java.lua.
| Key | Action |
|---|---|
<leader>jo |
Organize imports |
<leader>jc |
Extract constant |
<leader>jv |
Extract variable |
<leader>jt |
Run test class |
<leader>jT |
Run nearest test method |
K |
Hover documentation |
gd |
Go to definition |
| Key | Action |
|---|---|
<leader>o… |
opencode actions (prefix <leader>o) |
<leader>out |
Toggle tool output |
<leader>our |
Toggle reasoning output |
<leader>oum |
Toggle max messages |
| Key | Action |
|---|---|
<F11> |
Toggle fullscreen |
<C-S-n> |
Open Alacritty in the current directory |
~/.config/nvim/
├── init.lua # Entry: leader → options → lazy → keymaps
├── ftplugin/
│ ├── java.lua # jdtls + Lombok + debug/test bundles
│ └── markdown.lua # Markdown keymaps (buffer-local, thin wiring)
├── lib/
│ └── lombok.jar # Fallback Lombok agent
├── queries/
│ └── typescript/injections.scm # Treesitter injection override
├── lua/
│ ├── config/ # Non-plugin core
│ │ ├── options.lua # vim.opt, autocmds, htmlangular detection
│ │ ├── lazy.lua # lazy.nvim bootstrap + { import = "plugins" }
│ │ ├── keymaps.lua # Global keymaps + layout manager
│ │ ├── terminal.lua # <C-j> terminal toggle + terminal-normal <Esc> mode
│ │ ├── indent.lua # Custom indentexpr (VSCode onEnterRules)
│ │ ├── indent_sync.lua # Formatter config → buffer indent sync
│ │ ├── comments.lua # Single gc comment toggle (formatter-aware)
│ │ ├── templates.lua # BufNewFile skeletons (9 languages)
│ │ ├── markdown.lua # Obsidian-style markdown engine (pure logic)
│ │ ├── lsp_import.lua # <M-CR> missing-import action + <C-M-CR> action list
│ │ └── prompt_keys.lua # Telescope prompt readline-style input keys
│ └── plugins/ # One file per plugin group (lazy specs)
└── tests/ # Headless specs
Load order — init.lua sets mapleader first, then config.options, then
config.lazy (which bootstraps lazy.nvim and imports every module in
lua/plugins/), then config.keymaps last so plugin commands like Telescope
exist when the mappings are created, then config.templates for the
BufNewFile autocmd and config.comments for gc. config.keymaps also
requires config.terminal, which captures the startup working directory for
the <C-j> terminal.
File templates — config/templates.lua seeds a buffer only when it is a
brand-new, empty file (BufNewFile + pristine-buffer check). Java derives the
package line when the path contains src/main/java or src/test/java, and
the class name is PascalCased from the filename. Files outside those project
layouts are treated as standalone and get a main method. Python is
intentionally left empty. Headers get #ifndef guards named after the file. C# uses the filename
as the class name with a Main(string[] args) entry point and a file-scoped
namespace derived from the nearest .csproj (project name + subfolders);
without a .csproj it emits no namespace. Go, Kotlin and Rust get a bare
main; PHP gets the <?php opener. Ruby is intentionally empty. Neo-tree
creates files without opening a buffer, so its file_added event feeds
zero-byte files through the same builder (seed_file).
Filetype layer — ftplugin/ and the FileType autocmds in
config/options.lua and lua/plugins/treesitter.lua attach LSP, indent and
keymaps only where they belong, keeping global state untouched.
Markdown split — lua/config/markdown.lua holds the logic (cursor
geometry, span detection, list parsing and renumbering) and defines no
keymaps; ftplugin/markdown.lua only binds keys to it. This is what makes the
engine testable headlessly.
Web indentation — for html, htmlangular, javascript, typescript
and JSX/TSX, Treesitter indent is deliberately disabled and indent.lua's rule
table (mode / before / before_not / current / action) drives
indentexpr instead.
Layout manager — an autocmd group in config/keymaps.lua pins Neo-tree to
the far left (~25%) and the opencode pane to the far right, re-applying widths
whenever windows open, close or the editor resizes.
The specs run against a clean Neovim (-u NONE) and load only the code
under test:
nvim --headless -u NONE -l tests/indent_sync_spec.lua
nvim --headless -u NONE -l tests/keymap_word_nav_spec.lua
nvim --headless -u NONE -l tests/markdown_list_spec.lua
nvim --headless -u NONE -l tests/markdown_mapping_spec.lua
nvim --headless -u NONE -l tests/obsidian_checkbox_spec.lua
nvim --headless -u NONE -l tests/templates_spec.lua
nvim --headless -u NONE -l tests/comments_spec.lua
nvim --headless -u NONE -l tests/terminal_toggle_spec.lua
nvim --headless -u NONE -l tests/terminal_normal_mode_spec.lua
nvim --headless -u NONE -l tests/layout_column_spec.lua
nvim --headless -u NONE -l tests/neotree_mapping_spec.lua
nvim --headless -u NONE -l tests/lsp_import_spec.lua
nvim --headless -u NONE -l tests/prompt_keys_spec.lua
nvim --headless -u NONE -l tests/empty_buffer_spec.luaEach prints ok / FAIL lines and exits non-zero on failure, so they drop
into CI as-is.
| Spec | Covers |
|---|---|
keymap_word_nav_spec |
<C-h> / <C-l> aliases — normal, insert and all visual modes match <C-Left> / <C-Right> |
markdown_list_spec |
List parsing, continuation, renumbering |
markdown_mapping_spec |
ftplugin/markdown.lua bindings |
obsidian_checkbox_spec |
Normal-mode <Enter> never creates checkboxes |
templates_spec |
BufNewFile + seed_file templates (Java/C/C++/headers/C#/Go/Kotlin/Rust/PHP), package/namespace detection, existing files untouched |
comments_spec |
gc normal/line/block toggles, formatter-style blocks, // round-trip |
lsp_import_spec |
<M-CR> diagnostic targeting, import filter, smart candidate pick, <C-M-CR> mappings |
terminal_normal_mode_spec |
Terminal <Esc> leaves Terminal mode, normal-mode <Esc> reaches the running program, maps stay inside the terminal buffer |
layout_column_spec |
Stacked windows share one column: focusing text/terminal keeps the Neo-tree width; separate columns still resize the focused editor |
prompt_keys_spec |
Telescope prompt readline-style keys and first-press <Esc> close |
empty_buffer_spec |
Leftover [No Name] buffer/tab is removed after a file opens (including startup), restored as a normal placeholder when the last file closes, kept when no file is open |
Leader
-- init.lua
vim.g.mapleader = " "
vim.g.maplocalleader = "\\"Add a plugin — drop a new spec file into lua/plugins/; it is picked up
automatically by the { import = "plugins" } glob.
-- lua/plugins/my-plugin.lua
return {
"author/my-plugin.nvim",
event = "VeryLazy",
opts = {},
}Formatters — lua/plugins/conform.lua owns the per-filetype list;
lua/config/indent_sync.lua keeps the buffer's indent settings equal to what
the formatter will produce, so typing and saving never disagree.
| Filetypes | Formatter | Indent source |
|---|---|---|
c, cpp, objc, objcpp |
clang_format |
.clang-format (clang-format --dump-config), else buffer/global |
javascript, typescript, *react, html, htmlangular, css, scss, less, json, jsonc, yaml |
prettier |
prettier config (JSON or resolveConfig), else buffer/global |
lua |
stylua |
stylua.toml |
kotlin |
ktlint |
.editorconfig (built-in Neovim support) |
go |
LSP fallback (gopls) |
gofmt standard: tabs, tabstop 4 |
rust |
LSP fallback (rust-analyzer) |
rustfmt.toml, else 4 spaces |
java |
LSP fallback (jdtls) |
jdtls / project settings |
Precedence: project formatter config → .editorconfig → global 4 spaces.
When a project config exists the formatter gets no indent arguments (it reads
its own config); otherwise the arguments are generated from the buffer
options, so both sides use the same width. Every resolver is wrapped in
pcall with timeouts, and failures fall back to the current buffer options —
a bad or missing config never blocks editing or saving. Markdown is
intentionally excluded so Prettier cannot re-wrap prose.
Line width — 120 columns by default, still project-first:
| Language | Behaviour |
|---|---|
| C/C++ no project config | ColumnLimit: 120 |
| Prettier | --print-width 120 unless the project config sets printWidth |
| Rust | max_width=120 unless rustfmt.toml sets max_width |
| Java | jdtls default is already 120; project settings untouched |
| Go / Kotlin | gofmt and ktlint do not reflow lines |
Lombok — resolved in this order: ./lib/lombok.jar in the current project
→ ~/.config/nvim/lib/lombok.jar → the jar bundled with jdtls.
Obsidian vaults — the workspaces table in lua/plugins/obsidian.lua
points at personal paths; replace them with your own.
- JDK path is hardcoded in
ftplugin/java.lua(/usr/lib/jvm/java-21-openjdk). Change it for your machine. render-markdown.nvimloads from a local fork at~/dev/render-markdown.nvimbecause it carries a downstream patch. Without that checkout the plugin will not load — point thedirat your own copy or swap it for the upstream repo.lazy-lock.jsonis gitignored, so plugin versions are not pinned across machines.lib/lombok.jaris committed to give jdtls a Lombok agent even when a project ships none.- Some comments in the Lua sources are in Turkish.