I won’t talk too much about this since the most important thing is get started. As far as I understand, it is still vim basically, but neovim allows you to configure it with Lua (a programming language that is super easy to learn if you don’t know) instead of Vim script.
Configuration Location
~/.config/nvim if you are using Linux/MacOS. If the folder does not exist, create one.
Configuration File Structure
You can have your own preferred file structure, but mine looks like this to make me feel organized.
1 2 3 4 5
. ├── init.lua └── lua ├── core └── plugins
init.lua is the entry point.
1 2 3 4 5 6 7 8
-- Core configurations require("core.options") -- line number, indentation, etc require("core.keymaps") -- core keymaps, excluding plugins require("core.autocommands") -- user configured autocommands require("core.misc") -- other settings, language
-- Plugins require("plugins.lazy") -- use lazy to manage all plugins
Let’s do these configs one by one.
Core Configurations
Create files in lua/core folder, filename should match init.lua.
-- diagnostics vim.keymap.set("n", "[d", vim.diagnostic.goto_prev, { desc = "go to previous [D]iagnostic message" }) vim.keymap.set("n", "]d", vim.diagnostic.goto_next, { desc = "go to next [D]iagnostic message" })
Misc
1
vim.cmd("language en_US")
Autocommands
Autocommands gives us great flexibility to nicely configure our neovim just as the way we want. For example, if I’m writing an independent C++ program and I need to test the output along the way, I have to stop writing, and type compile command in terminal, and run the executable. This entire procedure can be automated using autocommand. Another useful case for me is when I code in Python or Go, I want to run tests every time I save the file and quickly find out which test failed. And this can also be solved by autocommand.
But this is rather large topic, check [[Neovim Autocommands]] for more details.
Plugins
Similar to VSCode, Neovim community is really active and you can almost find a plugin for whatever you need.
Lazy
We use lazy.nvim All the plugins is listed in lua/plugins/lazy.lua
We can see there are many plugins including theme, telescope etc. Let’s look at them one by one.
The installation and configuration for each plugin is included in a separate file. For example, ~/.config/nvim/lua/plugins/theme.lua is for theme plugins.
Theme
There are several options for theme plugin, you can search for it on the internet or GitHub. The one used by LazyVim is tokyonight.nvim. But I personally like catppuccin. And you can also edit color palette.
Like we have in many editors, a file tree is frequently used (although I prefer search for files in telescope, another plugin). The plugin here I use is nvim-neo-tree.
Leader key is defined as <SPACE> in ~/.config/nvim/lua/core/keymaps.lua.
Note that there is a keybinding for <SPACE>e. The function is to open the left side file tree and move cursor onto current file in the file tree window. And if it is opened already, close it.
You can set a different keybinding logic that meets your need, just check readme for nvim-neo-tree.
Navigation Between Windows
We often use split windows in vim. For example, the file tree window is also a split window.
If you don’t know what is window, check [[Tab, Window and Buffer in Vim]].
And we might need to navigate between our code window and file tree window with mouse. But we are using vim and vimmers don’t use mouse. So we need to navigate between windows with keyboard and to do this, try nvim-tmux-navigation.
For multiple lines, select the lines in visual mode
gc to apply single line comment for each line
gb to apply block comment
Take C/C++ as example language.
1
return0;
In normal mode, gcc will turn this line into
1
// return 0;
For multiple lines
1 2
i = 0x5f3759df - ( i >> 1 ); y = * ( float * ) &i;
select these two lines in visual mode, gc will give us
1 2
// i = 0x5f3759df - ( i >> 1 ); // y = * ( float * ) &i;
while gb gives
1 2
/* i = 0x5f3759df - ( i >> 1 ); y = * ( float * ) &i; */
Git Signs
When our code is in a git repository, we would like to see an indicator telling us which line is added or removed or modified. And gitsigns.nvim is designed for this use case.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
return { -- Adds git related signs to the gutter, as well as utilities for managing changes { "lewis6991/gitsigns.nvim", opts = { signs = { add = { text = "+" }, change = { text = "~" }, delete = { text = "_" }, topdelete = { text = "‾" }, changedelete = { text = "~" }, }, }, }, }
And leaving the opts blank will use default settings. And to see blame for current line, run vim command :Gitsigns toggle_current_line_blame. And you will see who last edited the current line.
See more detail on their GitHub page.
Which-key
which-key.nvim is helpful when you are not yet familiar with your keybindings. It shows possible follow-up keys in a popup.
return { -- Useful plugin to show you pending keybinds. { "folke/which-key.nvim", event = "VimEnter", -- Sets the loading event to 'VimEnter' config = function()-- This is the function that runs, AFTER loading require("which-key").setup()
We need different color and highlights for variables, functions and many others as we get for free in many modern IDEs. In neovim, we will use nvim-treesitter for syntax highlighting.
You may be seeing some warning from diagnostics for missing some fields like sync_install or ignore_install. It’s okay to ignore that warning and the plugin will work just fine.
Telescope
Telescope is probably the most frequently used plugin. It’s a fuzzy finder that helps me find files and content inside files.
I just copied this part from kickstart.nvim. Or maybe I did some minor modifications.
-- `cond` is a condition used to determine whether this plugin should be installed and loaded. cond = function() return vim.fn.executable("make") == 1 end, }, { "nvim-telescope/telescope-ui-select.nvim" }, { "nvim-tree/nvim-web-devicons", enabled = vim.g.have_nerd_font }, }, config = function() require("telescope").setup({ -- You can put your default mappings / updates / etc. in here -- All the info you're looking for is in `:help telescope.setup()` -- -- defaults = { -- mappings = { -- i = { ['<c-enter>'] = 'to_fuzzy_refine' }, -- }, -- }, -- pickers = {} defaults = { -- https://github.com/catppuccin/nvim/discussions/323?sort=top#discussioncomment-8653291 sorting_strategy = "ascending", layout_strategy = "flex", layout_config = { horizontal = { preview_cutoff = 80, preview_width = 0.55 }, vertical = { mirror = true, preview_cutoff = 25 }, prompt_position = "top", width = 0.87, height = 0.80, }, }, extensions = { ["ui-select"] = { require("telescope.themes").get_dropdown(), }, }, })
-- Enable Telescope extensions if they are installed pcall(require("telescope").load_extension, "fzf") pcall(require("telescope").load_extension, "ui-select")
-- Slightly advanced example of overriding default behavior and theme vim.keymap.set("n", "<leader>/", function() -- You can pass additional configuration to Telescope to change the theme, layout, etc. builtin.current_buffer_fuzzy_find(require("telescope.themes").get_dropdown({ winblend = 10, previewer = false, })) end, { desc = "[/] Fuzzily search in current buffer" })
vim.keymap.set("n", "<leader>fc", function() -- You can pass additional configuration to Telescope to change the theme, layout, etc. builtin.current_buffer_fuzzy_find() end, { desc = "[F]ind in [C]urrent buffer" })
-- It's also possible to pass additional configuration options. -- See `:help telescope.builtin.live_grep()` for information about particular keys vim.keymap.set("n", "<leader>f/", function() builtin.live_grep({ grep_open_files = true, prompt_title = "Live Grep in Open Files", }) end, { desc = "[F]ind [/] in Open Files" })
Simply speaking, LSP parses your code much more deeper than treesitter. For example
1
int foo = bar()
Treesitter knows that foo is an int variable and bar is a function. This is enough to have a nice highlighting of your code. But does the function bar exist? Maybe it is defined in another header file. Or does the function bar returns an integer?
LSP knows the context for current position, therefore it can also provide information for auto-completion. We’ll cover that in next plugin.
Except for auto-completion, the most frequently used feature provided by LSP are Go to definition/references and Hover documentation.
I map gd to Go to definition and gr as Go to references. If there are multiple references (most of the cases, yes there are), it will open a telescope floating window displaying all candidates.
Also, K is used to peek for document. Type K again to enter the peek window.
Finally, to manage LSP servers, we use mason.nvim.
Here is the file content of ~/.config/nvim/lua/plugins/lsp.lua
return { -- lsp { "neovim/nvim-lspconfig", dependencies = { "williamboman/mason.nvim", "williamboman/mason-lspconfig.nvim", "WhoIsSethDaniel/mason-tool-installer.nvim", { "j-hui/fidget.nvim", opts = {} }, { "folke/neodev.nvim", opts = {} }, }, config = function() vim.api.nvim_create_autocmd("LspAttach", { group = vim.api.nvim_create_augroup("kickstart-lsp-attach", { clear = true }), callback = function(event) local map = function(keys, func, desc) vim.keymap.set("n", keys, func, { buffer = event.buf, desc = "LSP: " .. desc }) end -- Jump to the definition of the word under your cursor. -- This is where a variable was first declared, or where a function is defined, etc. -- To jump back, press <C-t>. map("gd", require("telescope.builtin").lsp_definitions, "[G]oto [D]efinition")
-- Find references for the word under your cursor. map("gr", require("telescope.builtin").lsp_references, "[G]oto [R]eferences")
-- Jump to the implementation of the word under your cursor. -- Useful when your language has ways of declaring types without an actual implementation. map("gI", require("telescope.builtin").lsp_implementations, "[G]oto [I]mplementation")
-- Jump to the type of the word under your cursor. -- Useful when you're not sure what type a variable is and you want to see -- the definition of its *type*, not where it was *defined*. map("<leader>D", require("telescope.builtin").lsp_type_definitions, "Type [D]efinition")
-- Fuzzy find all the symbols in your current document. -- Symbols are things like variables, functions, types, etc. map("<leader>ds", require("telescope.builtin").lsp_document_symbols, "[D]ocument [S]ymbols")
-- Fuzzy find all the symbols in your current workspace. -- Similar to document symbols, except searches over your entire project. map( "<leader>ws", require("telescope.builtin").lsp_dynamic_workspace_symbols, "[W]orkspace [S]ymbols" )
-- Rename the variable under your cursor. -- Most Language Servers support renaming across files, etc. map("<leader>rn", vim.lsp.buf.rename, "[R]e[n]ame")
-- Execute a code action, usually your cursor needs to be on top of an error -- or a suggestion from your LSP for this to activate. map("<leader>ca", vim.lsp.buf.code_action, "[C]ode [A]ction")
-- Opens a popup that displays documentation about the word under your cursor -- See `:help K` for why this keymap. map("K", vim.lsp.buf.hover, "Hover Documentation")
-- WARN: This is not Goto Definition, this is Goto Declaration. -- For example, in C this would take you to the header. map("gD", vim.lsp.buf.declaration, "[G]oto [D]eclaration")
local client = vim.lsp.get_client_by_id(event.data.client_id) if client and client.server_capabilities.documentHighlightProvider then local highlight_augroup = vim.api.nvim_create_augroup("kickstart-lsp-highlight", { clear = false }) vim.api.nvim_create_autocmd({ "CursorHold", "CursorHoldI" }, { buffer = event.buf, group = highlight_augroup, callback = vim.lsp.buf.document_highlight, })
vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, { buffer = event.buf, group = highlight_augroup, callback = vim.lsp.buf.clear_references, }) end end, })
local capabilities = vim.lsp.protocol.make_client_capabilities() capabilities = vim.tbl_deep_extend("force", capabilities, require("cmp_nvim_lsp").default_capabilities()) local servers = { clangd = {}, gopls = {}, pyright = {}, -- rust_analyzer = {}, -- ... etc. See `:help lspconfig-all` for a list of all the pre-configured LSPs -- -- Some languages (like typescript) have entire language plugins that can be useful: -- https://github.com/pmizio/typescript-tools.nvim -- -- But for many setups, the LSP (`tsserver`) will work just fine tsserver = {}, --
lua_ls = { -- cmd = {...}, -- filetypes = { ...}, -- capabilities = {}, settings = { Lua = { completion = { callSnippet = "Replace", }, -- You can toggle below to ignore Lua_LS's noisy `missing-fields` warnings -- diagnostics = { disable = { 'missing-fields' } }, }, }, }, } require("mason").setup() local ensure_installed = vim.tbl_keys(servers or {}) vim.list_extend(ensure_installed, { "stylua", -- Used to format Lua code }) require("mason-tool-installer").setup({ ensure_installed = ensure_installed })
require("mason-lspconfig").setup({ handlers = { function(server_name) local server = servers[server_name] or {} -- This handles overriding only values explicitly passed -- by the server configuration above. Useful when disabling -- certain features of an LSP (for example, turning off formatting for tsserver) server.capabilities = vim.tbl_deep_extend("force", {}, capabilities, server.capabilities or {}) require("lspconfig")[server_name].setup(server) end, }, }) end, }, }
Auto-completion
There are several options for auto-complete plugins, I use nvim-cmp.
I use <Ctrl-n> and <Ctrl-p> for next and previous completion candidate and <Ctrl-y> for confirm autocompleting with the selected candidate. You can configure to use <TAB>, <Shift-TAB>and <Enter> if you like.
return { -- autocomplete { "hrsh7th/nvim-cmp", event = "InsertEnter", dependencies = { -- Snippet Engine & its associated nvim-cmp source { "L3MON4D3/LuaSnip", build = "make install_jsregexp", dependencies = { -- `friendly-snippets` contains a variety of premade snippets. -- See the README about individual language/framework/plugin snippets: -- https://github.com/rafamadriz/friendly-snippets { "rafamadriz/friendly-snippets", config = function() require("luasnip.loaders.from_vscode").lazy_load() end, }, }, }, "saadparwaiz1/cmp_luasnip",
-- Adds other completion capabilities. -- nvim-cmp does not ship with all sources by default. They are split -- into multiple repos for maintenance purposes. "hrsh7th/cmp-nvim-lsp", "hrsh7th/cmp-path", }, config = function() local cmp = require("cmp") local luasnip = require("luasnip") luasnip.config.setup({})
-- For an understanding of why these mappings were -- chosen, you will need to read `:help ins-completion` -- -- No, but seriously. Please read `:help ins-completion`, it is really good! mapping = cmp.mapping.preset.insert({ -- Select the [n]ext item ["<C-n>"] = cmp.mapping.select_next_item(), -- Select the [p]revious item ["<C-p>"] = cmp.mapping.select_prev_item(),
-- Accept ([y]es) the completion. -- This will auto-import if your LSP supports it. -- This will expand snippets if the LSP sent a snippet. ["<C-y>"] = cmp.mapping.confirm({ select = true }),
-- If you prefer more traditional completion keymaps, -- you can uncomment the following lines --['<CR>'] = cmp.mapping.confirm { select = true }, --['<Tab>'] = cmp.mapping.select_next_item(), --['<S-Tab>'] = cmp.mapping.select_prev_item(),
-- Manually trigger a completion from nvim-cmp. -- Generally you don't need this, because nvim-cmp will display -- completions whenever it has completion options available. ["<C-Space>"] = cmp.mapping.complete({}),
-- Think of <c-l> as moving to the right of your snippet expansion. -- So if you have a snippet that's like: -- function $name($args) -- $body -- end -- -- <c-l> will move you to the right of each of the expansion locations. -- <c-h> is similar, except moving you backwards. ["<C-l>"] = cmp.mapping(function() if luasnip.expand_or_locally_jumpable() then luasnip.expand_or_jump() end end, { "i", "s" }), ["<C-h>"] = cmp.mapping(function() if luasnip.locally_jumpable(-1) then luasnip.jump(-1) end end, { "i", "s" }),
-- For more advanced Luasnip keymaps (e.g. selecting choice nodes, expansion) see: -- https://github.com/L3MON4D3/LuaSnip?tab=readme-ov-file#keymaps }), sources = { { name = "nvim_lsp" }, { name = "luasnip" }, { name = "path" }, }, }) end, }, }
Autoformat
In many languages, autoformat is pretty useful, especially when we copy paste code from another source and the format can be a mess. Also one important usage for me is that it helps me to format imports in the convention of my organization.
return { { "windwp/nvim-autopairs", event = "InsertEnter", config = true, -- use opts = {} for passing setup options -- this is equalent to setup({}) function }, }
LeetCode
This is a fancy plugin that let you solve LeetCode problems using your most familiar editor, neovim.
To access the problems, we need to login with session token.
I’d love it more if it supports problem sets (I haven’t finished the Top Interview 150 problem set yet).
The plugin helps me download the test cases, compile and run the test cases and display the result in a neat UI.
Also, I can create a template for each language and it will help me initialize code files for each problem, but we need to tell the plugin where is our template file in configuration.