Neovim as an IDE for programming

In this how-to I am going to show you how to configure neovim to act like a small and efficient UI IDE for various programming languages.
Neovim is going to be configured in lua.

Before beginning you need the following packages installed, or ports build:
-> editors/neovim
-> www/npm
-> Clangd (if you want to have C/C++ language-server-protocol support. It should already be available in FreeBSD)

Let us begin with the folder structure.
I strived to make a clean folder structure for easy modifications and extensions, should they be needed.

1) Delete your nvim directories if you have already used neovim before, or create a backup.
Neovim usually places its directories in ~/.config, ~/.local/share, ~/.local/state.
Delete these directories by issuing:
Code:
rm -rf ~/.config/nvim ~/.local/share/nvim ~/.local/state/nvim

If you wish to create a backup issue:
Code:
mv ~/.config/nvim ~/.config/nvim.bak
In case you want to revert back to your old settings.

2) Create the folder structure.
For creating the structure issue the following command:
Code:
mkdir -p ~/.config/nvim/lua/{configs,plugin-manager,plugins,lsp-config}

Now we are going to install some plugins, starting with the plugin manager lazy vim.
Lazy vim is a plugin manager which only loads plugins if needed (lazy loads them).
Create a file called lazy.lua in ~/.config/nvim/lua/plugins/manager by issuing:
Code:
touch ~/.config/nvim/lua/plugins/manager/lazy.lua

Paste the following content into the file.
Code:
-- Neovims plugin manager

-- Bootstrap lazy vim
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not (vim.uv or 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)

-- Configure lazy vims behaviour
require("lazy").setup({
    spec = {
        -- Tell lazy vim where it should look for plugins
        { import = "plugins.enhancements" },
        { import = "plugins.lsp-config" }
    },
    checker = {
        enabled = true, -- Check for plugin updates upon start
        notify = false, -- Notify on update
    },
    change_detection = {
        notify = false, -- Notify on detection change
    },
})

-- Enable transparent background in lazys UI
function Transparent(color)
    color = color
    vim.cmd.colorscheme(color)
    vim.api.nvim_set_hl(0, "Normal", { bg = "none" })
    vim.api.nvim_set_hl(0, "NormalFloat", { bg = "none" })
end
Transparent()

Next we are going to install plugins I consider to be enhancements for the current neovim editor.
Choose only plugins you think that you will eventually need.

1) nvim-treesitter
This plugin is a configurations and abstraction layer.
To put it into simple words, it is a tool which abstracts the steps to create .so files for different programming lanugages in order to allow precise syntax highlighting.
Create a nvim-treesitter.lua file in ~/.config/nvim/lua/plugins/enhancements and paste the following content into it:
Code:
-- Configuration and abstraction layer, adds more syntax highlighting for different kind of languages

return {
    "nvim-treesitter/nvim-treesitter",
    event = { "BufReadPre", "BufNewFile" },
    build = ":TSUpdate",
    dependencies = {
        "windwp/nvim-ts-autotag",
    },
    config = function()
        local treesitter = require("nvim-treesitter.configs")

        treesitter.setup({
            highlight = {
                enable = true,
                additional_vim_regex_highlighting = false,
            },
            indent = { enable = true },
            autotag = {
                enable = true,
            },
            ensure_installed = {
                "bash",
                "c",
                "c_sharp",
                "cmake",
                "comment",
                "cpp",
                "css",
                "csv",
                "diff",
                "disassembly",
                "html",
                "javascript",
                "json",
                "lua",
                "make",
                "python",
            },
            incremental_selection = {
                enable = true,
                keymaps = {
                    init_selection = "<C-space>",
                    node_incremental = "<C-space>",
                    scope_incremental = false,
                    node_decremental = "<bs>",
                },
            },
            rainbow = {
                enable = true,
                disable = { "html" },
                extended_mode = false,
                max_file_lines = nil,
            },
            context_commentstring = {
                enable = true,
                enable_autocmd = false,
            },
        })
    end,
}
In the ensure_installed section place only the languages you want to be syntax highlighted.
For a list of supported languages visit their github page.

2) My preferred color theme and transparency
This one just serves as an idea for your preferred color theme.
Create a nvim-color-theme.lua file in ~/.config/nvim/lua/plugins/enhancements and paste the following content into it:
Code:
-- My preferred color theme for neovim and transparency feature for the background

return {
    "Mofiqul/dracula.nvim",
    "xiyaowong/transparent.nvim",
    lazy = false,
    priority = 1000,
    config = function()
        vim.cmd("colorscheme dracula")
    end,
}
To enable a transparent background (should you decide to want it), issue :TransparentEnable while being inside the editor.

3) nvim-autopairs
This plugin enables to create a pair of:
-> parentheses ()
-> square brackets []
-> triangle brackets <>
-> braces {}
-> single quotation marks ''
-> double quotation marks ""
-> backquotes ``
it puts your mouse inside the pair each time you want to create one.
Create a nvim-autopairs.lua file in ~/.config/nvim/lua/plugins/enhancements and paste the following content into it:
Code:
-- Enable autopair capability for neovim

return {
    "windwp/nvim-autopairs",
    event = "InsertEnter",
    config = function()
        require("nvim-autopairs").setup({
            disable_filetype = { "TelescopePrompt", "vim" },
        })
    end,
}

4) nvim-cmp (auto completion suggestions for many languages)
Create a nvim-cmp.lua file in ~/.config/nvim/lua/plugins/enhancements and paste the following content into it:
Code:
 -- Completion capabilities for neovim

return {
    "hrsh7th/nvim-cmp",
    event = "InsertEnter",
    dependencies = {
        "hrsh7th/cmp-buffer", -- Source for text in buffer
        "hrsh7th/cmp-path", -- Source for file system paths
        {
            "L3MON4D3/LuaSnip", -- Snippet Engine
            version = "v2.*",
            build = "make install_jsregexp", -- Allow lsp-snippet-transformations
        },
        "rafamadriz/friendly-snippets", -- Preconfigured snippets for different languages
        "onsails/lspkind.nvim", -- VS-Code like pictograms
    },
    config = function()
        local cmp = require("cmp")
        local lspkind = require("lspkind")
        local luasnip = require("luasnip")

        require("luasnip.loaders.from_vscode").lazy_load() -- Required for friendly-snippets to work

        -- Settings for the appearance of the completion window
        vim.api.nvim_set_hl(0, "CmpNormal", { bg = "#000000", fg = "#ffffff" })
        vim.api.nvim_set_hl(0, "CmpSelect", { bg = "#000000", fg = "#b5010f" })
        vim.api.nvim_set_hl(0, "CmpBorder", { bg = "#000000", fg = "#b5010f" })

        cmp.setup({
            snippet = {
                expand = function(args)
                    luasnip.lsp_expand(args.body)
                end,
            },
            mapping = cmp.mapping.preset.insert({
                ["<C-d>"] = cmp.mapping.scroll_docs(-4),
                ["<C-f>"] = cmp.mapping.scroll_docs(4),
                ["<C-Space>"] = cmp.mapping.complete(),
                ["<C-e>"] = cmp.mapping.close(),
                ["<CR>"] = cmp.mapping.confirm({
                    behavior = cmp.ConfirmBehavior.Replace,
                    select = true,
                }),
            }),
            sources = cmp.config.sources({
                { name = "nvim_lsp" },
                { name = "luasnip" },
                { name = "buffer" },
                { name = "path" },
            }),
            window = {
                 completion = {
                     border = "rounded",
                     winhighlight = "Normal:CmpNormal,CursorLine:CmpSelect,FloatBorder:CmpBorder",
                 }
            },
        })
        vim.cmd([[
        set completeopt=menuone,noinsert,noselect
        highlight! default link CmpItemKind CmpItemMenuDefault
        ]])
    end,
}
Set your theme for the window to your liking, or delete vim.api.* and the window section to get VS-Code like colors for the completion window.
If luasnip reports problems during installation, close lazy vim, and the editor.
Then reopen the editor, and type in :Lazy.
Now luasnip should be installed, and working.
The problem happens in luasnips make file due to a OS check from line 37 to 42, I guess.

5) nvim-neo-tree
Create a nvim-neo-tree.lua file in ~/.config/nvim/lua/plugins/enhancements and paste the following content into it:
Code:
-- Tree like file system manager

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

6) nvim-telescope
Create a nvim-telescope.lua file in ~/.config/nvim/lua/plugins/enhancements and paste the following content into it:
Code:
-- A highly extendable fuzzy finder over lists

return {
    "nvim-telescope/telescope.nvim",
    tag = "0.1.6",
    dependencies = { "nvim-lua/plenary.nvim" },
}

7) nvim-lualine
Create a nvim-telescope.lua file in ~/.config/nvim/lua/plugins/enhancements and paste the following content into it:
Code:
-- A lightweight statusline for neovim

-- Bubbles config for lualine
-- Author: lokesh-krishna
-- MIT license, see LICENSE for more details.

-- stylua: ignore
local colors = {
  blue   = '#80a0ff',
  cyan   = '#79dac8',
  black  = '#080808',
  white  = '#c6c6c6',
  red    = '#ff5189',
  violet = '#d183e8',
  grey   = '#303030',
}

local bubbles_theme = {
  normal = {
    a = { fg = colors.black, bg = colors.violet },
    b = { fg = colors.white, bg = colors.grey },
    c = { fg = colors.white },
  },

  insert = { a = { fg = colors.black, bg = colors.blue } },
  visual = { a = { fg = colors.black, bg = colors.cyan } },
  replace = { a = { fg = colors.black, bg = colors.red } },

  inactive = {
    a = { fg = colors.white, bg = colors.black },
    b = { fg = colors.white, bg = colors.black },
    c = { fg = colors.white },
  },
}

return {
    "nvim-lualine/lualine.nvim",
    dependencies = { "nvim-tree/nvim-web-devicons" },
    config = function()
        require('lualine').setup {
            options = {
                theme = bubbles_theme,
                component_separators = '',
                section_separators = { left = '', right = '' },
            },
            sections = {
                lualine_a = { { 'mode', separator = { left = '' }, right_padding = 2 } },
                lualine_b = { 'filename', 'branch' },
                lualine_c = {'%=', --[[ add your center compoentnts here in place of this comment ]] },
                lualine_x = {},
                lualine_y = { 'filetype', 'progress' },
                lualine_z = { { 'location', separator = { right = '' }, left_padding = 2 }, },
            },
            inactive_sections = {
                lualine_a = { 'filename' },
                lualine_b = {},
                lualine_c = {},
                lualine_x = {},
                lualine_y = {},
                lualine_z = { 'location' },
            },
            tabline = {},
            extensions = {},
        }
    end,
}
Choose a config based on your preferences.

8) nvim-lspconfig
This one is quite interesting as it offers best community effort configurations for various lsp servers.
For me it works with different languages like a charm.
Create a nvim-lspconfig.lua file in ~/.config/nvim/lua/plugins/lsp-config and paste the following content into it:
Code:
-- Best effort collection of LSP server configurations

return {
    "neovim/nvim-lspconfig",
    event = { "BufReadPre", "BufNewFile" },
    dependencies = {
        "hrsh7th/cmp-nvim-lsp",
        { "folke/lazydev.nvim", opts = {} },
    },
    config = function()
        -- local nvim_lsp = require("lspconfig")
        -- local protocol = require("vim.lsp.protocol")
        local on_attach = function(client, bufnr)
            -- Format on save
            if client.server_capabilities.documentFormattingProvider then
                vim.api.nvim_create_autocmd("BufWritePre", {
                    group = vim.api.nvim_create_augroup("Format", { clear = true }),
                    buffer = bufnr,
                    callback = function()
                        vim.lsp.buf.format()
                    end,
                })
            end
        end
    end,
}
To get a list of supported LSP servers visit their github page.

Let us now create the editor configs as well as some keybindings to make the work with neovim easier.
Create a editor-settings.lua file in ~/.config/nvim/lua/configs and paste the following content into it:
Code:
-- Editor settings

vim.opt.number = true -- Print the line number in front of each line
vim.opt.relativenumber = true -- Show the line number relative to the line with the cursor in front of each line.
vim.opt.clipboard = "unnamedplus" -- uses the clipboard register for all operations except yank.
vim.opt.syntax = "on" -- When this option is set, the syntax with this name is loaded.
vim.opt.autoindent = true -- Copy indent from current line when starting a new line.
vim.opt.cursorline = true -- Highlight the screen line of the cursor with CursorLine.
vim.opt.expandtab = true -- In Insert mode: Use the appropriate number of spaces to insert a <Tab>.
vim.opt.shiftwidth = 4 -- Number of spaces to use for each step of (auto)indent.
vim.opt.tabstop = 4 -- Number of spaces that a <Tab> in the file counts for.
vim.opt.encoding = "UTF-8" -- Sets the character encoding used inside Vim.
vim.opt.ruler = true -- Show the line and column number of the cursor position, separated by a comma.
vim.opt.mouse = "a" -- Enable the use of the mouse. "a" you can use on all modes
vim.opt.title = true -- When on, the title of the window will be set to the value of 'titlestring'
vim.opt.hidden = true -- When on a buffer becomes hidden when it is |abandon|ed
vim.opt.ttimeoutlen = 0 -- The time in milliseconds that is waited for a key code or mapped key sequence to complete.
vim.opt.wildmenu = true -- When 'wildmenu' is on, command-line completion operates in an enhanced mode.
vim.opt.showcmd = true -- Show (partial) command in the last line of the screen. Set this option off if your terminal is slow.
vim.opt.showmatch = true -- When a bracket is inserted, briefly jump to the matching one.
vim.opt.inccommand = "split" -- When nonempty, shows the effects of :substitute, :smagic, :snomagic and user commands with the :command-preview flag as you type.
vim.opt.splitright = true -- When on, splitting a window will put the new windows to the right side of the current one
vim.opt.splitbelow = true -- When on, splitting a window will put the new window below the current one
vim.opt.termguicolors = true -- Enable terminal GUI colors in neovims UI
Adjust the file to your liking.

Create a keybindings.lua file in ~/.config/nvim/lua/configs and paste the following content into it:
Code:
-- Keybindings for neovim editor

-- Redefine map leader from "\" to " "
vim.g.mapleader = " "

-- Make mapping to keys easier
local function map(mode, lhs, rhs)
    vim.keymap.set(mode, lhs, rhs, { silent = true })
end

-- Save
map("n", "<leader>w", "<CMD>update<CR>")

-- Quit
map("n", "<leader>q", "<CMD>q<CR>")

-- Exit insert mode
map("i", "jk", "<ESC>")

-- NeoTree
map("n", "<leader>e", "<CMD>Neotree toggle<CR>")
map("n", "<leader>r", "<CMD>Neotree focus<CR>")

-- New Windows
map("n", "<leader>o", "<CMD>vsplit<CR>")
map("n", "<leader>p", "<CMD>split<CR>")

-- Window Navigation
map("n", "<C-h>", "<C-w>h")
map("n", "<C-l>", "<C-w>l")
map("n", "<C-k>", "<C-w>k")
map("n", "<C-j>", "<C-w>j")

-- Resize Windows
map("n", "<C-Left>", "<C-w><")
map("n", "<C-Right>", "<C-w>>")
map("n", "<C-Up>", "<C-w>+")
map("n", "<C-Down>", "<C-w>-")

-- Find files
map("n", "<leader>ff", "<cmd>Telescope find_files<cr>")

-- Show files in buffer
map("n", "<leader>fb", "<cmd>Telescope buffers<cr>")
The last two keybindings (Telescope find_files, and Telescope buffers) only work if plugin number 6 (nvim-telescope) is installed.

The last step is to create a init.lua file in ~/.config/nvim to ensure that every piece gets connected.
Paste the following contents into the init.lua file:
Code:
-- Load editor settings
require("configs.editor-settings")

-- Load keybindings
require("configs.keybindings")

-- Load plugin manager
require("plugins.manager.lazy")

-- Load corresponding LSP choosen by file extension
require("lspconfig").bashls.setup({})
require("lspconfig").clangd.setup({})
require("lspconfig").pyright.setup({})
These are the LSPs I am currently using.
Extend or shrink the LSP list to your liking.

If you want to install for example bashls or pyright issue the following command as root:
Code:
npm i -g bash-language-server
npm i -g pyright

To uninstall language server protocols issue the following command as root:
Code:
npm uninstall -g language-server-protocol-name

One thing to note, I am aware that there is a debug adapter protocol (dap) and a dap-ui out there for various languages, but I am not using them right now.
If the need should ever arise, I am going to update this how-to.

Hopefully this how-to will help other users get neovim set up faster and easier for their programming needs.
If you have questions, suggestions feel free to post them.
 
Last edited:
Done something similar in the past:

 
Done something similar in the past:

I think I saw that thread, and tried to apply some things.
The ansible language server is now outdated since October 9th, 2024, due to migration into VS-Code, I guess.
 
-- Load corresponding LSP choosen by file extension
require("lspconfig").bashls.setup({})
require("lspconfig").clangd.setup({})
require("lspconfig").pyright.setup({})
[/code]
These are the LSPs I am currently using.
Extend or shrink the LSP list to your liking.
Excellent write-up, I learned new things! Thank you.

My suggestion for the above though is:
Code:
vim.lsp.enable {
    'clangd',
    ...
}

And then you can adjust it to use the version of clangd installed in your ~/.config/nvim/after/lsp
directory. You then create a clangd.lua there with just:
Code:
return { cmd = { 'clangd20' }}

Obviously choose one that is installed (for me it was devel/llvm20). But as you already required nvim-lspconfig all you have to do is update keys that don't work, not the whole config.

I went the route of starting with kickstart-modular.nvim and ripping out `mason` as it has no precompiled FreeBSD LSPs and will just fail. That is available in my github fork for the curious.
 
Back
Top