Setting Up Neovim

Posted on 2025-07-22 11:33:19

Written by Psicaster

Welcome fellow nerds! This post is geared toward Linux Users, but a lot of it can still be applied to those who want to use Neovim on Windows. If you ARE using Windows, I highly recommend some version of WSL. But if you're interested in Neovim, you're probably already there, eh?

Getting Started with Plugin Managers

There are a plethora of Plugin Managers out there, and they all have their merits. However, I have mainly used LazyVim's plugin manager in the past, and it is the one I'm the most comfortable with, so we are going to start there.

First, let's go ahead and create our neovim config files and directory tree.

mkdir -p ~/.config/nvim
cd ~/.config/nvim
mkdir  lua/ lua/plugins/
touch init.lua opts.lua

This will get the framework up and running. Let's quickly run over each of these files and directories.

The Package Manager Lazy.nvim

To install the package manager, we are first going to need to clone it to Neovim's share folder, located at ~/.local/share/nvim/ If this doesn't exist yet, you can create it.

I have had considerable success using the following method:

mkdir -p ~/.local/share/nvim/lazy/
git clone --filter=blob:none https://github.com/folke/lazy.nvim.git --branch=stable ~/.local/share/nvim/lazy/lazy.nvim/

Then adding the following to init.lua

local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
  vim.fn.system({
    "git",
    "clone",
    "--filter=blob:none",
    "https://github.com/folke/lazy.nvim.git",
    "--branch=stable", -- latest stable release
    lazypath,
  })
end
vim.opt.rtp:prepend(lazypath)

This will ensure that we have the LazyVim plugin manager installed properly. I have had mixed success with adding the block to the init.lua first, which as you can probably see, is SUPPOSED to check if the path exists, and pull down a new copy of LazyVim if it doesn't. Sometimes it actually does as intended.

Open up Neovim and type :Lazy. If the Lazy interface appears, you've done it! You can now begin grabbing Neovim plugins.

Time to consider Plugins

There are a lot of Neovim plugins, and it is very easy to fall down the rabbit hole. I myself have fallen deep into it, and often found myself lost in the pit, not remembering which plugins do which. And when something updates? Well, you might as well forget it.

That isn't to say that you can't just go install all of the neovim plugins, and start breaking things. It's a good way to learn. But I don't particularly have a huge need for all of the plugins. Mostly, I want to download an LSP, get some code actions and code completion together, and call it good. Though most people aren't exactly fans of Neovim's default color scheme either, so we may want to address that as well.


Look at all the pretty colors!

First, let's address colors. Everyone has their version of comfortable color schemes, and I'm certainly no different. I prefer something dark, but with enough contrast between colors that I can easily identify text highlights. Personally, I'm a fan of the sweet.nvim color palette, but other popular choices are catpuccin, tokyonight, and many others

Regardless of which theme you choose, we should be ready to install the color scheme.

Let's use Neovim to edit our Neovim configs. Seems appropriate.

nvim ~/.config/nvim/lua/plugins/colors.lua

Now, we obviously need to put something in here. But what? Well, let's take a second to understand how Lazyvim works with Lua.

Lazyvim will look in the lua/ and lua/plugins/ folders for a lua script, as described above. But what output is it looking for? Tables. That's right. Lua Tables. If you're familiar with JSON, it's a similar format. I'm not going to go too much into what a Lua Table is, or how they work, as lua.org has provided some wonderful documentation on what they are, how they work, and how to format one. What we need to know for this current context however, is that they are a data type (the only data type in fact) used in Lua to hold data. Lazy looks for these tables to tell it what plugins to download and manage, and their many options. So, to start with, we need to get into our colors.lua file, and tell it to return a table.

return { 
  "haize-uwu/sweet.nvim",
  lazy = false,
  config = function()
      vim.cmd.colorscheme("sweet")  
  end
}

But what is this table doing? Let's go line-by-line.

Okay. Now we save the file, and reload Neovim, and hopefully it should download, and then load, our color scheme!


Great. Neovim looks pretty. But it's still just a text editor.

Very true, so let's start looking into other plugins we want. Personally, I am using Neovim for programming, so I want to give it some nice IDE functionality. To start with, we're going to need something called an LSP, or Language Server Protocol.

How do we get an LSP, I hear you ask, and how do I know which LSP to use?! Fear not, for the wonderful William Boman has created a great plugin for Neovim that will help us pull down our first LSP. This plugin is called Mason.nvim.

Like before, we need to create a new file. nvim ~/.config/nvim/lua/plugins/mason.lua And like before, we are going to be returning a table. For now, it's going to be VERY simple.

return {
"williamboman/mason.nvim"
}

That's it? Yes. That's it! Now save the file and reload Neovim again. As you can see, the Lazy package manager is going to pull down Mason aaaaaaand... Nothing happens... Why? Well, we need to load Mason's setup. We can do this in a couple of ways, but for the sake of keeping plugins contained in their own files, we are going to edit mason.lua.

Remember the config = function() line from the colors.lua script? Well, we're going to do it again, but this time we're going to put in require("mason").setup({})

it should look something like this:

return {
"williamboman/mason.nvim",
config = function()
    require("mason").setup()
end
}

We're going to leave the setup function empty for now. This will load the Mason defaults, which will be fine for our current purposes. Save the file, and reload neovim. Now we should be able to type :Mason and load the Mason interface. From here, we can load a variety of LSPs, Formatters and Linters. For now, let's just grab an LSP for Lua, which might help us with our Neovim configs.

In the Mason interface, let's hit 2, to filter just LSPs, and then filter by language using Ctrl+F. This will pull up a long list piped into less. Scroll to the bottom, and enter 103 for Lua. We will improve this experience shortly with Telescope, but for now we will navigate down to lua-language-server (also known as lua_ls), and hit i. This will install the Lua LSP server. Pretty painless right?

Okay. I have this LSP, but how the hell do I use it?

A perfectly reasonable question. The answer is: BY INSTALLING MORE PLUGINS! Yeah, I know. But bear with me. These next steps should be pretty similar to the previous steps, and ultimately fairly painless.

Remember how I said we're going to keep plugins contained in their own file? Well, I lied. But it was for the sake of learning so... don't hate me!

Let's go back into our mason.lua file. We're going to add a couple more plugins directly. Firstly, we are going to pull down William Boman's mason-lspconfig.nvim plugin, then the neovim/nvim-lspconfig plugin. The first thing we need to do, is separate the plugins into their own sub-tables. Tables in Lua are always anonymous, so we don't have to declare any variables. Just surround the plugin, and its config, in their own table { }. Like this:

return{

    {
        "williamboman/mason.nvim",
    config = function()
        require("mason").setup()
        end
    },

}

Okay. We have our new nested table. Now, we're going to need to load the mason-lspconfig.nvim plugin. Let's keep going by creating another nested table, just like we did for mason.nvim, to load our next plugin in the chain.

return {
    {
        "williamboman/mason.nvim",
    config = function()
        require("mason").setup()
        end
    },

    {
        "williamboman/mason-lspconfig.nvim",
        config = function()
            require("mason-lspconfig").setup({
                ensure_installed = { "lua_ls" } -- We can always add more LSPs here later.
        })
end
    },
}

Starting to look like a real script eh? So, what's this ensure_installed field? I'm sure it's obvious that it is ensuring that our LSPs are installed on setup. This can be handy if we're pulling our configs across different workstations. Now, let's get our nvim-lspconfig plugin installed and configured. Continuing in our mason.lua file, we're going to add a third table to our plugin chain.

return {
    {
        "williamboman/mason.nvim",
    config = function()
        require("mason").setup()
        end
    },

    {
        "williamboman/mason-lspconfig.nvim",
        config = function()
            require("mason-lspconfig").setup({
                ensure_installed = { "lua_ls" }
        })
    },
    {
        "neovim/nvim-lspconfig",
        config = function()
        local lspconfig = require("lspconfig")
        lspconfig.lua_ls.setup({})
    }
}

And that should do it. Save, reload Neovim and our LSP is now working. We don't yet have auto-complete, but we do have error reporting. You can test this by opening up any of our lua files, such as init.lua, and you should immediately see some feedback. We also can see a yellow "W" next to any lines that have a warning associated with them.


It's at this point I would like to introduce you to Telescope and Treesitter

Honestly, We probably should have done these first, and anyone who's set up an nvim config from a tutorial will probably agree. But to me, getting the core built out first was more important. Now that we can confirm that things are working, let's get a little bit of QOL in the mix.

Let's start with Treesitter. This is a parser generation tool, and will improve our syntax highlighting. The first step should be familiar by now: create treesitter.lua in our lua/plugins/ directory.

And of course, we're going to return our table

return {
  {
    "nvim-treesitter/nvim-treesitter",
    build = ":TSUpdate",
    config = function()
      local config = require("nvim-treesitter.configs")
      config.setup({
        auto_install = true,
        highlight = { enable = true },
        indent = { enable = true },
      })
    end
  }
}

There's some new context here! Let's address it now.

We can ensure that our parser is installed by typing :TSInstall lua if we have any doubts that it's installed.

And now, Telescope

Another great quality of life extension. This is a powerful fuzzy finder, and is highly customizable. Before we move forward, however, we might want to grab a few packages from our linux install (I assume we're using linux here... If you're on Windows, and you are using Neovim, just switch. Do it. It's easier than you think). I believe that ripgrep is available for Windows though.. Two packages immediately come to mind, and both will help us with Telescope's functionality.

In Debian based distros, we can get it these both using # apt install fzf ripgrep -y If you use Arch (btw), you can grab the same packages using pacman.

Now. Let's install Telescope. Our first plugin with a dependency! As usual, we are going to create a telescope.lua file, and return a table.

return {
    'nvim-telescope/telescope.nvim',
      dependencies = { 'nvim-lua/plenary.nvim' }
    }

Well that was easy. Now we have Telescope installed. We can use :Telescope find_files to see that Telescope is installed. But I want to get a bit more out of Telescope than just a fuzzy find. I want it to load for selection windows too. So, as before, let's build out a config function, and put in a little extra functionality, as well as adding an extension mod for Telescope to give us some nice popout menus.

return {
  {
    "nvim-telescope/telescope-ui-select.nvim",
  },
  {
    "nvim-telescope/telescope.nvim",
    dependencies = { "nvim-lua/plenary.nvim" },
    config = function()
      require("telescope").setup({
        extensions = {
          ["ui-select"] = {
            require("telescope.themes").get_dropdown({}),
          },
        },
      })
      require("telescope").load_extension("ui-select")
    end,
  },
}

I know I'm throwing a lot at you again, but let's go over it briefly. First, we added in the telescope-ui-select plugin at the top, which we will implement in our config.

Great! Now, to show that this is working, let's load up :Mason. Remember before when I said we were going to improve the experience for the filters? Hit that Ctrl+C, and look how much nicer that is than a piped list! Even better, you can now type what you're looking for! See? Quality of life!


And finally we implement autocomplete.

It's time, friends! Let's get some Autocomplete going! I mostly waited this long because I really couldn't decide on which plugin I wanted to use. But here it is! blink.nvim. Or more specifically, the blink.cmp module. I haven't looked much into the complete suite of blink mini-mods, but I do like blink.cmp, so that's what I'm using.

Let's start by making our cmp.lua file, and creating our data table.

return {
  'saghen/blink.cmp',
  -- optional: provides snippets for the snippet source
  dependencies = 'rafamadriz/friendly-snippets',

  opts = {
    keymap = { preset = 'default' },

    appearance = {
      use_nvim_cmp_as_default = true,
      nerd_font_variant = 'mono'
    },
    signature = {enabled = true}
  },
}

And it's that simple! We now have some auto-complete. Though, I'm not a huge fan of the default hotkeys. is currently set to accept a suggestion, but for the sake of it, I really like Tab. For ease, you can use 'super-tab' instead of default to give you tab completion, but it does mean the dreaded ARROW KEYS are used for navigation. If that's not your speed either, of course, you can always read the documentation, and set custom keys.


Let's talk about file exploration.

Here in a few we're going to set up some super sweet hot keys, for Telescope to help us quickly find files that we want to access, but sometimes I like just having a file tree to navigate. I present to you: neo-tree.nvim. I moved from editors like VSCode and even some of the Jetbrains products, and one thing that just makes me feel warm and fuzzy inside is being able to pull up a file tree. It also helps me sometimes to visualize my file structure in a product. So, let's get to it!

return {
    "nvim-neo-tree/neo-tree.nvim",
    branch = "v3.x",
    dependencies = {
        "nvim-lua/plenary.nvim",
        "nvim-tree/nvim-web-devicons",
        "MunifTanjim/nui.nvim",
    },
}

And to test it, let's just use :Neotree filesystem reveal left to test that it's working! If you see your file tree open up on the left (or right, if you're a weirdo), then you've done it!

But... let's be honest. Typing out :Neotree filesystem reveal left every time I want to check out the file tree is a bit obtuse. So is :Telescope find_files and really all the other commands that these plugins need. Why don't we set up some keybinds, and do something kinda exciting.

Let's make our own vim motions.

If you're setting up Neovim, you're probably at least a little familiar with vim motions. If you're not, then... go do :Tutor. This isn't a write-up on Vim Motions, it's a write up on Neovim plugins.

But, before we get right into the motions, we need to set some configuration settings. Let's go ahead and pop open init.lua (for now, we might split it out into its own options file at some point).

At the top of the file, let's go ahead and add the following:

-- Sensible tabs. Especially in the terminal space.
vim.cmd("set expandtab")
vim.cmd("set tabstop=2")
vim.cmd("set softtabstop=2")
vim.cmd("set shiftwidth=2")
vim.g.mapleader = " "

-- This is optional, but if you plan to do much with markdown plugins, it is helpful.
vim.o.conceallevel = 3

-- Disabling the swapfile. I don't need it, and honestly it has always caused me issues.
vim.opt.swapfile = false

vim.keymap.set('n', '<leader>h', ':nohlsearch<CR>')
vim.wo.number = true --line number

The most important one here is vim.g.mapleader = " ". That will set our <leader> key to space. This can be set to whatever leader key you want. I'm not your momma ((I hope...)).

Okay. Let's get this show on the road. I like to put the keybinds in the file of their related plugins. This will avoid issues in the future if I need to remove a plugin for one reason or another. Let's start with Neo-Tree.

dependencies ={ ... },
config = function()
    vim.keymap.set("n", "<leader>e", ":Neotree filesystem reveal left<CR>", {})
    vim.keymap.set("n", "<leader>nf", ":Neotree buffers reveal float<CR>", {})
end,
}

Reload Neovim, and you'll now be able to toggle the Neotree window with e. nf will also allow us to take a look at our file buffer as well. Both handy. Remember, these can be set to whatever you want, I just like these for myself.

    config = function()
      require("telescope").setup({
        extensions = {
          ["ui-select"] = {
            require("telescope.themes").get_dropdown({}),
          },
        },
      })
      local builtin = require("telescope.builtin")
      vim.keymap.set("n", "<leader>ff", builtin.find_files, {})
      vim.keymap.set("n", "<leader>fg", builtin.live_grep, {})
      vim.keymap.set("n", "<leader>fo", builtin.oldfiles, {})

      require("telescope").load_extension("ui-select")
    end,
  },
}

We're just expanding the config function here. After the setup({}) function we can add our keybinds. First, we're going to set a builtin variable, so we can call the telescope builtins. Then we're adding three new binds. <leader>ff for find files. <leader>fg for grepping our files, and <leader>fo for finding previously worked files (oldfiles).

So... why am I using f as a precursor for these? for find of course! Telescope offers all sorts of features for fuzzy searching and grepping through our files. You can use whatever key combination you like, of course, these are just the ones that make the most sense to me. Save, exit out of neovim, and then give them a try! We have more, of course, but this is a good start. Now, let's start getting used to using our plugins. Navigate to ~/.config/nvim if you're not already there, and nvim . to open up the folder. Now let's use <leader>ff to pull up our next file to add keybinds to. Let's do a search for mason.lua. Neat, huh?!

Okay, at the bottom of our mason.lua file, let's go ahead and add some keybinds to our config function. I like to add them just above the end declaration so that they're easy to find for future edits.

... [previous config] ...
            vim.keymap.set("n", "K", vim.lsp.buf.hover, {})
            vim.keymap.set("n", "cd", vim.lsp.buf.definition, {})
        vim.keymap.set({ "n", "v" }, "<leader>ca", vim.lsp.buf.code_action, {})
    end,
    },
}

Reload, and now we have some nice code actions. K is the default for neovim's lsp.buf.hover, which will show information about the symbol under the cursor in a floating window. There's a lot of information [here] about all of the vim.lsp functions, and I would suggest spending some time familiarizing yourself with them if you're planning on doing any programming or using LSPs with Neovim.

We also have added some functionality to find/goto definition of a function, and my favorite, code actions. This will suggest some actions to resolve warnings or errors, and is a fairly common feature of IDEs.

And oh so much more.

Hopefully this serves to get you at least a basic setup for Neovim. Go explore the world of Neovim plugins. Almost all of them have some sort of setup and configuration documentation. I plan to add some code formatting, install some additional LSPs for languages I'm working with, and some other little QOL plugins. I also would suggest checking out kiwi.nvim, which is a great "Wiki" plugin, which can make note taking in an organized way feel real nice.

Now go. And be back before sundown. You don't want the Grue to get you.