A handcrafted Neovim configuration, designed to be understood, extended, and mastered.
This repository contains a Neovim configuration that is modern, readable, and highly modular, intended as an evolving foundation for building a development environment close to an IDE.
The main goals of this project are:
- 🧩 Maximum modularity: Each feature is isolated in a clearly identified file.
- 🧠 Readability and pedagogy: The configuration remains understandable, even after months of inactivity.
- 🚀 Scalability: Adding a plugin or an LSP layer is done via a simple file.
- 🔧 Declarative approach: Lazy.nvim is used as the central manager.
Once the structure is in place, maintenance essentially involves adding or adjusting modules, without modifying the core configuration.
This project is still under construction...
- Neovim ≥ 0.11
- Lua as the configuration language
- lazy.nvim: plugin manager
- mason.nvim / mason-lspconfig.nvim: installation and management of LSPs
- nvim-lspconfig (via the
vim.lspAPI) - Tree-sitter for syntax analysis
- Lazydev to improve the Lua LSP experience with configuration files.
- nerdfonts
.
├── docs
│ ├── autocommands.md
│ ├── commands.md
│ └── lsp-nvcrafted.md
├── init.lua
├── lazy-lock.json
├── lua
│ ├── core
│ │ ├── autocmds.lua
│ │ ├── bootstrap.lua
│ │ ├── format
│ │ │ └── conform.lua
│ │ ├── highlights
│ │ │ └── diagnostics_theme_nord.lua
│ │ ├── keymaps.lua
│ │ ├── lsp
│ │ │ ├── capabilities.lua
│ │ │ ├── on_attach.lua
│ │ │ ├── servers.lua
│ │ │ └── tools.lua
│ │ ├── options.lua
│ │ └── spell.lua
│ └── plugins
│ ├── coding
│ │ ├── autopairs.lua
│ │ ├── blink.lua
│ │ ├── conform.lua
│ │ ├── lazydev.lua
│ │ ├── telescope.lua
│ │ └── treesitter.lua
│ ├── init.lua
│ ├── lsp
│ │ ├── config
│ │ │ ├── lua_ls.lua
│ │ │ ├── pyright.lua
│ │ │ └── ruff.lua
│ │ ├── init.lua
│ │ └── mason.lua
│ ├── tools
│ │ └── which_key.lua
│ └── ui
│ ├── aerial.lua
│ ├── alpha.lua
│ ├── colorscheme.lua
│ ├── lualine.lua
│ ├── neo_tree.lua
│ └── trouble.lua
├── README.fr.md
├── README.md
└── snippets
└── python.json
NvCrafted includes technical documentation complementing the README, organized by topic in the docs/ folder (written exclusively in French).
| File | Content |
|---|---|
autocommands.md |
Description of the autocommand groups defined in core/autocmds.lua |
commands.md |
List of commands available in NvCrafted |
lsp-nvcrafted.md |
Complete LSP support architecture: installation flow, orchestration, role of on_attach and capabilities, extension conventions |
architecture.md |
An explanation of the architectural design of NvCrafted |
The main entry point (purely declarative):
- Bootstraps lazy.nvim via
core.bootstrap(isolation of lazy.nvim installation logic) - Defines leaders
- Loads
coremodules - Initializes Lazy with automatic plugin imports
init.lua (root)
│
├── core.bootstrap
│ └── installs lazy.nvim if missing
│
├── system configuration
│ ├── disable unused providers (Perl, Ruby)
│ ├── dedicated Neovim Python (~/.venvs/neovim)
│ └── append Mason to PATH
│
├── leader keys (Space / Backslash)
│
├── core/ modules (plugin-independent)
│ ├── core.spell
│ ├── core.options
│ ├── core.keymaps
│ └── core.autocmds
│
└── lazy.setup("plugins")
└── plugins/init.lua
└── automatic subdirectory scan
├── plugins/coding/
├── plugins/ui/
├── plugins/tools/
└── plugins/lsp/
└── init.lua
├── servers.lua → LSP activation
└── tools.lua → Mason tools
The order is intentional: core/ modules are loaded before Lazy, ensuring that options and leader keys are in place before any plugin is initialized.
Contains the fundamental Neovim configuration, independent of plugins.
| File | Role |
|---|---|
options.lua |
Neovim options (vim.opt) |
keymaps.lua |
Global keybindings |
autocmds.lua |
Autocommands |
spell.lua |
Custom dictionary |
bootstrap.lua |
lazy.nvim startup |
lsp/servers.lua |
Source of truth for LSP servers |
👉 These files do not depend on any plugin and can be read as "pure Neovim configuration."
-
Create a Lua file in the corresponding folder, for example:
lua/plugins/ui/for an interface pluginlua/plugins/coding/for a code editing pluginlua/plugins/tools/for cross-cutting tools
-
The file must return a table compatible with Lazy.nvim. Minimal example:
return {
"author/pluginname.nvim",
config = function()
-- plugin-specific configuration here
end
}Some plugins involve configuration logic rich enough to justify a dedicated file in core/. NvCrafted then applies a two-level separation:
| Level | File | Role |
|---|---|---|
| Declaration | plugins/<domain>/<plugin>.lua |
Declares the plugin to Lazy, delegates configuration |
| Specification | core/<domain>/<plugin>.lua |
Holds the actual logic, independent from Lazy |
plugins/coding/conform.lua only declares the plugin and calls the core module:
return {
"stevearc/conform.nvim",
opts = function()
require("core.format.conform").setup()
end,
}All the actual logic — formatters per file type, dynamic Python selection, format-on-save — lives in core/format/conform.lua.
- the plugin configuration contains business logic (conditions, functions, autocommands)
- it is likely to be reused by other modules
- it should remain readable and testable independently from Lazy's lifecycle
A plugin whose configuration fits in a few lines of static options does not need this split : opts = { ... } directly in the plugin file is enough.
Lazy.nvim provides two ways to configure a plugin. The choice depends on the nature of the configuration.
Use when the configuration consists of values passed directly to the plugin:
-- plugins/coding/autopairs.lua
opts = {
check_ts = true,
ts_config = {
lua = { "string" },
},
}Lazy automatically calls plugin.setup(opts). No additional code is needed.
Use when the configuration requires code: autocommands, pcall protection, manual API calls, etc.
-- plugins/coding/treesitter.lua
config = function()
local ok, ts_configs = pcall(require, "nvim-treesitter.configs")
if not ok then return end
ts_configs.setup({ ... })
endWhen a plugin has static options and code to run after initialization, both can coexist. opts is then received as the second argument of config:
-- plugins/coding/blink.lua
opts = {
keymap = { ... },
completion = { ... },
},
config = function(_, opts)
require("blink.cmp").setup(opts) -- opts forwarded here
require("luasnip.loaders.from_vscode") -- additional code
.lazy_load({ paths = { ... } })
vim.cmd([[hi LspFloatBorder ...]]) -- manual highlights
end,The _ as the first argument is the plugin itself (unused here, ignored by convention).
| Situation | Key to use |
|---|---|
| Static options only | opts |
Code to execute (API calls, autocommands, pcall) |
config |
| Static options + additional code | opts + config |
When in doubt, prefer opts: it is shorter, more readable, and Lazy handles the setup() call automatically.
Keybindings are distributed across three distinct spaces depending on their scope.
All keybindings that are always active, regardless of context, are defined here. This includes shortcuts that invoke plugins, as they are part of NvCrafted's global interface:
-- core/keymaps.lua
map("n", "<leader>ff", ":Telescope find_files<CR>", { desc = "Chercher fichiers" })
map("n", "<leader>ee", ":Neotree<CR>", { desc = "Ouverture de Neotree" })Keybindings that only make sense in a specific context (popup, floating menu) stay in the plugin file:
-- plugins/coding/blink.lua
opts = {
keymap = {
["<CR>"] = { "accept", "fallback" },
["<Tab>"] = { "select_next", "fallback" },
["<S-Tab>"] = { "select_prev", "fallback" },
},
}These keybindings are managed by blink.cmp itself and have no effect outside the completion menu. Defining them in core/keymaps.lua would serve no purpose.
A third space exists for LSP keybindings: they are defined in on_attach and only activate when an LSP server is attached to the current buffer.
-- core/lsp/on_attach.lua
vim.keymap.set("n", "gd", vim.lsp.buf.definition, opts)
vim.keymap.set("n", "<leader>cr", vim.lsp.buf.rename, opts)LSP keybindings belong in on_attach, never in core/keymaps.lua — they must not pollute buffers where no LSP is attached.
Adding an LSP server follows a declarative two-level approach.
-
Server Declaration Add the server name in
lua/core/lsp/servers.luaExample:return { "lua_ls", "pyright", "rust_analyzer", }
👉 This file is the source of truth:
- Used by Mason for installation
- Used by the LSP orchestrator for activation
-
Specific Configuration Create a file named after the server:
lua/plugins/lsp/config/<server_name>.lua. File structure:return { settings = { -- specific LSP configuration } }
Key Principle: An LSP server works without specific configuration. A layer is only loaded if a dedicated file exists.
This file is the aggregation point for plugins. It contains no direct configuration, only logical imports:
- Scans the
lua/plugins/directory to retrieve all subdirectories. - Transforms each subdirectory into an entry
{ import = "plugins.<name>" }. - Returns a table directly usable by
require("lazy").setup(). Each subfolder represents a functional domain.
👉 Key Principles:
- No plugin is manually declared in init.lua.
- Each plugin has its own file.
- The philosophy is to only configure what differs from default values, to keep files short and explicit. For example:
autopairs.luaonly redefines Tree-sitter integration.
Plugins related to the user interface:
- status bar (lualine)
- file explorer (neo-tree)
- welcome screen (alpha)
- color theme (Nord) - Yes, I'm a die-hard fan of this theme.
- Diagnostics visualization (Trouble)
- Viewing the structure of the current file (and navigating) (aerial)
Plugins enhancing the code editing experience:
- auto-pairs
- Tree-sitter
- Telescope
- formatting (conform)
- Lazydev
- Trouble
Cross-cutting tools (e.g., which-key) that do not directly fit into UI or coding.
The LSP support and tool installation are structured across four distinct levels.
- Explicit list of active LSP servers
- No logic, no plugin dependency
- Explicit list of non-LSP Mason tools: formatters (
black,stylua,prettier), linters (ruff), and other binaries. - Same philosophy as
servers.lua: a pure list, no logic.
These two files are the sources of truth for the environment. Everything that needs to be installed is declared here, and nowhere else.
Consumes both lists:
mason-lspconfiginstalls the servers fromservers.luamason-tool-installerinstalls the tools fromtools.luamason.nvimexecutes the installations
No functional decisions are made here.
For each server in servers.lua:
- applies shared
on_attachandcapabilities - loads the optional overlay
lsp/config/<server>.luaif it exists - registers the server via
vim.lsp.config()
A server works without an overlay. An overlay is only loaded if the corresponding file exists.
Snippets are stored in ~/.config/nvim/snippets/ in JSON format (compatible with VSCode).
Snippets are dynamically loaded using lazy_load:
-- plugins/coding/blink.lua
require("luasnip.loaders.from_vscode").lazy_load({
paths = { vim.fn.stdpath("config") .. "/snippets" },
})
**blink.cmp** is configured to include **luasnip** as a completion source:
```lua
sources = { -- Priority completion sources
default = { "lsp", "buffer", "snippets", "path" },
},NvCrafted integrates a spell-checking system adapted for code and comments.
- Dictionaries used: English (en), French (fr), and a custom code dictionary.
- Automatic creation: On first launch, the
code.utf-8.addfile is created with frequent technical words and compiled intocode.utf-8.spl. - Targeted spell-check: Active only in comments and strings.
- Automatic addition: Words validated with
zgare added tocode.utf-8.addand recompiled into.spl. - Compatibility: Works from the first launch, with Neo-tree and all buffers, without downloading external dictionaries.
- Neovim ≥ 0.11
- Git
# Clone the repository
git clone https://github.com/Krystof2so/NvCrafted.git ~/.config/nvim
# Launch Neovim
nvimLazy.nvim will automatically install the plugins on the first launch.
- 📦 One plugin = one file
- 🧠 Readability
- 🧪 No black boxes
- 🧩 Incremental extension
This configuration is designed as a personal workbase, but structured enough to serve as a reference or starting point.
My goal is to maintain an organization and configuration that is as simple and modular as possible. One specificity = one file... nothing more basic.
The comments inserted in each file are entirely in French (sorry to English speakers), because I find that Neovim is generally poorly documented in French (or even esoteric configurations).
- Gradual addition of plugins (UI, DAP, tests, refactoring...)
- Improvement of LSP integrations (layers)
- And anything else that comes to mind, while staying true to the project philosophy
Free to use, modify, and share.
✨ If you are looking for a modular and easily understandable Neovim configuration, this repository is for you.