Neovim LSP to replace VSCode

If you are looking for a much simpler solution to replacing VSCode with a terminal editor I suggest reading my minimal vim post.

TLDR;

All the code for this is availible nvim-code

Setup

This post is going to assume that neovim 0…


This content originally appeared on DEV Community and was authored by Cason Adams

If you are looking for a much simpler solution to replacing VSCode with a terminal editor I suggest reading my minimal vim post.

TLDR;

All the code for this is availible nvim-code

Setup

This post is going to assume that neovim 0.6+ is installed and ready to go. From that point lets lay down the directory structure we are going to be dealing with.

Neovim defaults to using ${HOME}/.config/nvim as the configuration directory. I am going to build off that as the base for config files.

nvim
├── after
│   └── ftplugin
│       └── python.lua
├── init.lua
├── lua
│   ├── _lsp.lua
│   ├── _options.lua
│   ├── _plugins.lua
│   ├── _statusline.lua
│   ├── _telescope.lua
│   ├── _treesitter.lua
│   └── _whichkey.lua

init.lua

require("_options")
require("_plugins")
require("_lsp")
require("_treesitter")
require("_telescope")
require("_whichkey")
require("_statusline")

vim.cmd("colorscheme walh-gruvbox")

_options.lua

I'm not going to cover all the options available, but here are some common ones that can be used or omitted. Heavily inspired by lunarvim.

vim.g.mapleader = " "

vim.g.border_style = "rounded"
vim.g.markdown_fenced_languages = {
    "bash=sh",
}

vim.opt.backup = false -- creates a backup file
vim.opt.clipboard = "" -- don't use clipboard
vim.opt.cmdheight = 1 -- more space in the neovim command line for displaying messages
vim.opt.colorcolumn = "99999" -- fixes indentline for now
vim.opt.completeopt = { "menuone", "noselect" }
vim.opt.conceallevel = 0 -- so that `` is visible in markdown files
vim.opt.cursorline = true -- highlight the current line
vim.opt.expandtab = true -- convert tabs to spaces
vim.opt.fileencoding = "utf-8" -- the encoding written to a file
vim.opt.foldexpr = "" -- set to "nvim_treesitter#foldexpr()" for treesitter based folding
vim.opt.foldmethod = "manual" -- folding set to "expr" for treesitter based folding
vim.opt.hidden = true -- required to keep multiple buffers and open multiple buffers
vim.opt.hlsearch = true -- highlight all matches on previous search pattern
vim.opt.ignorecase = true -- ignore case in search patterns
vim.opt.list = true
vim.opt.listchars = "tab:│ ,trail:·,nbsp:+"
vim.opt.number = true -- set numbered lines
vim.opt.numberwidth = 1 -- set number column width to 2 {default 4}
vim.opt.pumheight = 10 -- pop up menu height
vim.opt.relativenumber = false -- set relative numbered lines
vim.opt.scrolloff = 4 -- is one of my fav
vim.opt.shiftwidth = 2 -- the number of spaces inserted for each indentation
vim.opt.showmode = false -- we don't need to see things like -- INSERT -- anymore
vim.opt.sidescrolloff = 4
vim.opt.signcolumn = "yes" -- always show the sign column otherwise it would shift the text each time
vim.opt.smartcase = true -- smart case
vim.opt.smartindent = true -- make indenting smarter again
vim.opt.spell = false -- disable spell checking
vim.opt.spelllang = "en" -- language for spell checking
vim.opt.splitbelow = true -- force all horizontal splits to go below current window
vim.opt.splitright = true -- force all vertical splits to go to the right of current window
vim.opt.swapfile = false -- creates a swapfile
vim.opt.tabstop = 2 -- insert 2 spaces for a tab
vim.opt.termguicolors = false -- set term gui colors (most terminals support this)
vim.opt.timeoutlen = 500 -- timeout length
vim.opt.title = true -- set the title of window to the value of the titlestring
vim.opt.titlestring = "%<%F - nvim" -- what the title of the window will be set to
vim.opt.undodir = vim.fn.stdpath("cache") .. "/undo"
vim.opt.undofile = true -- enable persistent undo
vim.opt.updatetime = 300 -- faster completion
vim.opt.wrap = true -- display lines as one long line
vim.opt.writebackup = false -- if a file is being edited by another program (or was written to file while editing with another program) it is not allowed to be edited

vim.opt.showtabline = 2 -- always show tabs
vim.opt.laststatus = 2 -- hide statusline

_plugins.lua

I have to admit using neovims builtin lsp is nice but it comes with the requirement to install many plugins to get a similar experience with VSCode.

local fn = vim.fn
local install_path = fn.stdpath("data") .. "/site/pack/packer/start/packer.nvim"
if fn.empty(fn.glob(install_path)) > 0 then
    packer_bootstrap = fn.system({
        "git",
        "clone",
        "--depth",
        "1",
        "https://github.com/wbthomason/packer.nvim",
        install_path,
    })
end

return require("packer").startup(function()
    use({
        "L3MON4D3/LuaSnip",
        "casonadams/walh",
        "folke/trouble.nvim",
        "hrsh7th/cmp-buffer",
        "hrsh7th/cmp-cmdline",
        "hrsh7th/cmp-nvim-lsp",
        "hrsh7th/cmp-path",
        "hrsh7th/nvim-cmp",
        "jose-elias-alvarez/null-ls.nvim",
        "neovim/nvim-lspconfig",
        "nvim-lua/lsp-status.nvim",
        "nvim-treesitter/nvim-treesitter",
        "saadparwaiz1/cmp_luasnip",
        "tamago324/nlsp-settings.nvim",
        "wbthomason/packer.nvim",
        "williamboman/nvim-lsp-installer",
    })
    use({
        "rafamadriz/friendly-snippets",
    })
    use({
        "nvim-telescope/telescope.nvim",
        requires = { "nvim-lua/plenary.nvim" },
    })
    use({
        "nvim-lualine/lualine.nvim",
        requires = { "kyazdani42/nvim-web-devicons", opt = true },
    })
    use({
        "folke/which-key.nvim",
        config = function()
            require("which-key").setup({})
        end,
    })
    use({
        "terrortylor/nvim-comment",
        config = function()
            require("nvim_comment").setup({})
        end,
    })
    use({
        "lewis6991/gitsigns.nvim",
        config = function()
            require("gitsigns").setup({ yadm = { enable = true } })
        end,
    })
    use({
        "ethanholz/nvim-lastplace",
        event = "BufRead",
        config = function()
            require("nvim-lastplace").setup({
                lastplace_ignore_buftype = { "quickfix", "nofile", "help" },
                lastplace_ignore_filetype = { "gitcommit", "gitrebase", "svn", "hgcommit" },
                lastplace_open_folds = true,
            })
        end,
    })
    -- Automatically set up your configuration after cloning packer.nvim
    -- Put this at the end after all plugins
    if packer_bootstrap then
        require("packer").sync()
    end
end)

_lsp.lua

local cmp = require("cmp")
local lsp_status = require("lsp-status")

local win = require("lspconfig.ui.windows")
local _default_opts = win.default_opts

win.default_opts = function(options)
    local opts = _default_opts(options)
    opts.border = "rounded"
    return opts
end

-- statusline progress setup
lsp_status.config({
    current_function = false,
    show_filename = false,
    diagnostics = false,
    status_symbol = "",
    select_symbol = nil,
    update_interval = 200,
})

-- completion setup
cmp.setup({
    snippet = {
        expand = function(args)
            -- vim.fn["vsnip#anonymous"](args.body)
            require("luasnip").lsp_expand(args.body) -- For `luasnip` users.
            -- vim.fn["UltiSnips#Anon"](args.body)
        end,
    },
    mapping = {
        ["<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({ select = false }),
        ["<Tab>"] = cmp.mapping(cmp.mapping.select_next_item(), { "i", "s" }),
        ["<S-Tab>"] = cmp.mapping(cmp.mapping.select_prev_item(), { "i", "s" }),
    },
    sources = {
        { name = "nvim_lsp" },
        { name = "luasnip" },
        -- { name = "ultisnips" },
        -- { name = "vsnip" },
        { name = "buffer" },
        { name = "path" },
    },
})

-- helper function for mappings
local m = function(mode, key, result)
    vim.api.nvim_buf_set_keymap(0, mode, key, "<cmd> " .. result .. "<cr>", {
        noremap = true,
        silent = true,
    })
end

-- function to attach completion when setting up lsp
local on_attach = function(client)
    lsp_status.register_progress()
    lsp_status.on_attach(client)

    -- Mappings.
    m("n", "ga", "lua vim.lsp.buf.code_action()")
    m("n", "gD", "lua vim.lsp.buf.declaration()")
    m("n", "gd", "lua vim.lsp.buf.definition()")
    m("n", "ge", "lua vim.lsp.diagnostic.goto_next()")
    m("n", "gE", "lua vim.lsp.diagnostic.goto_prev()")
    m("n", "gi", "lua vim.lsp.buf.implementation()")
    m("n", "gr", "lua vim.lsp.buf.references()")
    m("n", "K", "lua vim.lsp.buf.hover()")
    -- m("n", "<space>rn", "lua vim.lsp.buf.rename()")
    m("n", "gl", "lua vim.lsp.diagnostic.show_line_diagnostics()")
    -- m("n", "<space>f", "lua vim.lsp.buf.formatting()")
end

-- setup lsp installer
local lsp_installer = require("nvim-lsp-installer")
-- Provide settings first!
lsp_installer.settings({
    ui = {
        icons = {
            server_installed = "✓",
            server_pending = "➜",
            server_uninstalled = "✗",
        },
    },
})
lsp_installer.on_server_ready(function(server)
    local opts = {
        on_attach = on_attach,
        capabilities = require("cmp_nvim_lsp").update_capabilities(vim.lsp.protocol.make_client_capabilities()),
        flags = {
            debounce_text_changes = 150,
        },
    }
    server:setup(opts)
end)

-- lsp settings
require("nlspsettings").setup()

-- diagnostics
vim.diagnostic.config({
    virtual_text = false,
    underline = true,
    float = {
        source = "always",
    },
    severity_sort = true,
    --[[ virtual_text = {
      prefix = "»",
      spacing = 4,
    }, ]]
    signs = true,
    update_in_insert = false,
})

_treesitter.lua

require'nvim-treesitter.configs'.setup {
  ensure_installed = "maintained",
  sync_install = false,
  highlight = {
    enable = true,
    additional_vim_regex_highlighting = false,
  },
}

_telescope.lua

require("telescope").setup({
    defaults = {
        border = true,
        layout_strategy = "bottom_pane",
        layout_config = {
            height = 0.30,
            width = 1.00,
        },
        -- path_display = { "shorten" },
        sorting_strategy = "ascending",
    },
})

require("trouble").setup({
  icons=false
})

_whichkey.lua

local which_key = {
    setup = {
        plugins = {
            marks = true,
            registers = true,
            presets = {
                operators = false,
                motions = false,
                text_objects = false,
                windows = true,
                nav = true,
                z = true,
                g = true,
            },
            spelling = { enabled = true, suggestions = 20 },
        },
        icons = {
            breadcrumb = "»",
            separator = "➜",
            group = "+",
        },
        window = {
            border = "none", -- none, single, double, shadow
            position = "bottom", -- bottom, top
            margin = { 1, 0, 1, 0 },
            padding = { 2, 2, 2, 2 },
        },
        layout = {
            height = { min = 4, max = 25 },
            width = { min = 20, max = 50 },
            spacing = 3,
        },
        hidden = { "<silent>", "<cmd>", "<Cmd>", "<CR>", "call", "lua", "^:", "^ " },
        show_help = true,
    },

    opts = {
        mode = "n",
        prefix = "<leader>",
        buffer = nil,
        silent = true,
        noremap = true,
        nowait = true,
    },
    vopts = {
        mode = "v",
        prefix = "<leader>",
        buffer = nil,
        silent = true,
        noremap = true,
        nowait = true,
    },
    -- NOTE: Prefer using : over <cmd> as the latter avoids going back in normal-mode.
    -- see https://neovim.io/doc/user/map.html#:map-cmd
    vmappings = {},
    mappings = {
        ["c"] = { ":BufferClose!<CR>", "Close Buffer" },
        ["e"] = { ":Telescope file_browser <CR>", "File Browser" },
        ["f"] = { ":Telescope find_files <CR>", "Find File" },
        ["h"] = { ":nohlsearch<CR>", "No Highlight" },
        b = {
            name = "Buffers",
            l = { ":Telescope buffers<CR>", "List Buffers" },
            b = { ":b#<cr>", "Previous" },
            d = { ":bd<cr>", "Delete" },
            f = { ":Telescope buffers <cr>", "Find" },
            n = { ":bn<cr>", "Next" },
            p = { ":bp<cr>", "Previous" },
        },
        p = {
            name = "Packer",
            c = { ":PackerCompile<cr>", "Compile" },
            i = { ":PackerInstall<cr>", "Install" },
            r = { ":lua require('lvim.utils').reload_lv_config()<cr>", "Reload" },
            s = { ":PackerSync<cr>", "Sync" },
            S = { ":PackerStatus<cr>", "Status" },
            u = { ":PackerUpdate<cr>", "Update" },
        },
        l = {
            name = "LSP",
            a = { ":Telescope lsp_code_actions<cr>", "Code Action" },
            d = {
                ":Telescope lsp_document_diagnostics<cr>",
                "Document Diagnostics",
            },
            w = {
                ":Telescope diagnostics<cr>",
                "Workspace Diagnostics",
            },
            f = { ":lua vim.lsp.buf.formatting()<cr>", "Format" },
            i = { ":LspInfo<cr>", "Info" },
            I = { ":LspInstallInfo<cr>", "Installer Info" },
            r = { ":lua vim.lsp.buf.rename()<cr>", "Rename" },
        },
        s = {
            name = "Search",
            b = { ":Telescope git_branches <cr>", "Checkout branch" },
            c = { ":Telescope colorscheme <cr>", "Colorscheme" },
            C = { ":Telescope commands <cr>", "Commands" },
            f = { ":Telescope find_files <cr>", "Find File" },
            h = { ":Telescope help_tags <cr>", "Find Help" },
            j = { ":Telescope jumplist <cr>", "Jumplist" },
            k = { ":Telescope keymaps <cr>", "Keymaps" },
            M = { ":Telescope man_pages <cr>", "Man Pages" },
            r = { ":Telescope oldfiles <cr>", "Open Recent File" },
            R = { ":Telescope registers <cr>", "Registers" },
            t = { ":Telescope live_grep <cr>", "Text" },
            n = { ":Telescope live_grep search_dirs={os.getenv('NOTES')} <cr>", "Notes" },
            p = {
                ":lua require('telescope.builtin.internal').colorscheme({enable_preview = true})<cr>",
                "Colorscheme with Preview",
            },
        },
        T = {
            name = "Treesitter",
            i = { ":TSConfigInfo<cr>", "Info" },
        },
        t = {
            name = "Diagnostics",
            t = { "<cmd>TroubleToggle<cr>", "trouble" },
            w = { "<cmd>TroubleToggle workspace_diagnostics<cr>", "workspace" },
            d = { "<cmd>TroubleToggle document_diagnostics<cr>", "document" },
            q = { "<cmd>TroubleToggle quickfix<cr>", "quickfix" },
            l = { "<cmd>TroubleToggle loclist<cr>", "loclist" },
            r = { "<cmd>TroubleToggle lsp_references<cr>", "references" },
        },
    },
}

function map(mode, lhs, rhs, opts)
        local options = { noremap = true, silent = true }
    if opts then
        options = vim.tbl_extend("force", options, opts)
    end
    vim.api.nvim_set_keymap(mode, lhs, rhs, options)
end

map("n", "H", ":bp<CR>")
map("n", "L", ":bn<CR>")

map("n", "<tab>", ":tabnext<CR>")
map("n", "<S-tab>", ":tabprevious<CR>")

map("n", "<C-h>", ":wincmd h<CR>")
map("n", "<C-j>", ":wincmd j<CR>")
map("n", "<C-k>", ":wincmd k<CR>")
map("n", "<C-l>", ":wincmd l<CR>")

local wk = require("which-key")
wk.setup(which_key.setup)

local opts = which_key.opts
local vopts = which_key.vopts

local mappings = which_key.mappings
local vmappings = which_key.vmappings

wk.register(mappings, opts)
wk.register(vmappings, vopts)

if which_key.on_config_done then
    which_key.on_config_done(wk)
end

_statusline.lua

local lsp_status = require("lsp-status")

local function lsp_progress()
    return lsp_status.status()
end

local function extract_highlight_colors(color_group, scope)
    if vim.fn.hlexists(color_group) == 0 then
        return nil
    end
    local color = vim.api.nvim_get_hl_by_name(color_group, true)
    if color.background ~= nil then
        color.bg = string.format("#%06x", color.background)
        color.background = nil
    end
    if color.foreground ~= nil then
        color.fg = string.format("#%06x", color.foreground)
        color.foreground = nil
    end
    if scope then
        return color[scope]
    end
    return color
end

local colors = {
    gray = 8,
    red = 9,
    green = 10,
    yellow = 11,
    blue = 12,
    magenta = 13,
    cyan = 14,
    white = 15,
    background = extract_highlight_colors("StatusLine", "bg"),
    foreground = 7,
}

local custom_theme = {
    normal = {
        a = { bg = colors.foreground, fg = colors.background },
        b = { bg = colors.background, fg = colors.foreground },
        c = { bg = colors.background, fg = colors.foreground },
    },
    insert = {
        a = { bg = colors.cyan, fg = colors.background },
        b = { bg = colors.background, fg = colors.cyan },
        c = { bg = colors.background, fg = colors.foreground },
    },
    visual = {
        a = { bg = colors.yellow, fg = colors.background },
        b = { bg = colors.background, fg = colors.yellow },
        c = { bg = colors.background, fg = colors.foreground },
    },
    replace = {
        a = { bg = colors.red, fg = colors.background },
        b = { bg = colors.background, fg = colors.red },
        c = { bg = colors.background, fg = colors.foreground },
    },
    command = {
        a = { bg = colors.magenta, fg = colors.background },
        b = { bg = colors.background, fg = colors.magenta },
        c = { bg = colors.background, fg = colors.foreground },
    },
    inactive = {
        a = { bg = colors.background, fg = colors.gray },
        b = { bg = colors.background, fg = colors.gray },
        c = { bg = colors.background, fg = colors.gray },
    },
}

local components = {
    mode = {
        function()
            return " "
        end,
        padding = { left = 0, right = 0 },
    },
    filename = {
        "filename",
    },
    diagnostics = {
        "diagnostics",
        sources = { "nvim_diagnostic" },
        symbols = { error = " ", warn = " ", info = " ", hint = " " },
    },
    treesitter = {
        function()
            local b = vim.api.nvim_get_current_buf()
            if next(vim.treesitter.highlighter.active[b]) then
                return ""
            end
            return ""
        end,
    },
    location = { "location" },
    progress = { "progress" },
    encoding = {
        "o:encoding",
        fmt = string.upper,
    },
    filetype = { "filetype" },
}

require("lualine").setup({
    options = {
        theme = custom_theme,
        icons_enabled = false,
        component_separators = { left = "", right = "" },
        section_separators = { left = "", right = "" },
        disabled_filetypes = { "dashboard", "NvimTree", "Outline" },
    },
    sections = {
        lualine_a = {},
        lualine_b = {
            components.filename,
        },
        lualine_c = {
            components.diff,
        },
        lualine_x = {
            lsp_progress,
            components.diagnostics,
        },
        lualine_y = {
            components.treesitter,
        },
        lualine_z = {},
    },
    inactive_sections = {
        lualine_a = {},
        lualine_b = {
            "filename",
        },
        lualine_c = {},
        lualine_x = {},
        lualine_y = {},
        lualine_z = {},
    },
    tabline = {
        lualine_a = {},
        lualine_b = { { "buffers" } },
        lualine_c = {},
        lualine_x = {},
        lualine_y = { { "tabs", mode = 0 } },
        lualine_z = {},
    },
    extensions = { "nvim-tree" },
})

Conclusion

Sooo much config I know! I am sorry about all of that. But it think having all of this config makes everything customizable for anyone. Take the settings I made as a grain of salt, I have an opinion about how vim should be for me. Hopefully this is just a foundation that someone can use to get up and running then modify things to the prefered settings. I also, left some commented out settings for further tinkering.

So if you have made it this far the next step is to try it out. Let see how it works.

main.py test

Create a file nvim main.py. On first load nvim is going to complain because none of the plugins are setup. Go ahead and just type q until the complaints end. We need restart nvim now that packer has been installed. qa then nvim main.py. Now we have packer we can :PackerSync this will install and update all the plugins defined in _plugins.lua. Close nvim one more time :qa. Now we got that over with then get on to testing out our setup. nvim main.py (notice that tree-sitter is installing a few things) let that happen in the background. Wait for tree-sitter to finish before exiting. Put this snippet in the file.

#!/usr/bin/env python

def echo(msg):
    print msg

if __name__ == "__main__":
    echo("Hello World!")

Save :w

install pyright server

Now we need to install the lsp server to work with python files. This can be done a few different ways fastest is to :LspInstall pyright, or one can access a "dashboard" with :LspInstallInfo and then use the help ? to see options. u will upgrade the server under the cursor, and i will install the server under the cursor.

Now that pyright is installed lets make sure it is working.

testing pyright

Notice line 4 msg is underlined and an E is indicated in the gitgutter. Move the cursor to line 4 and trigger the line diagnostics gl. If you see the popup you are configured correctly!

black formatter

I didn't go into very much detail about custom settings for the language servers that get installed or formatting and linting. Here is an examle of how to use black as the python formatter. We are using null-ls for the formatting here.

after/ftplugin/python.lua

 local null_ls = require("null-ls")

local sources = {
    null_ls.builtins.formatting.black,
}

null_ls.setup({ sources = sources })

Make sure black in installed pip install black. Lets try it out with our main.py file. Update it to the following, then save :w.

#!/usr/bin/env python
def echo(msg):
    print(msg)
if __name__ == "__main__":
    echo("Hello World!")
  • Note leader is space

Now to tell nvim to format with the defined formatter send leader lf.

And the result should be a nicely formatted python file.

python format

And that about wraps it up.


This content originally appeared on DEV Community and was authored by Cason Adams


Print Share Comment Cite Upload Translate Updates
APA

Cason Adams | Sciencx (2021-12-26T20:26:48+00:00) Neovim LSP to replace VSCode. Retrieved from https://www.scien.cx/2021/12/26/neovim-lsp-to-replace-vscode/

MLA
" » Neovim LSP to replace VSCode." Cason Adams | Sciencx - Sunday December 26, 2021, https://www.scien.cx/2021/12/26/neovim-lsp-to-replace-vscode/
HARVARD
Cason Adams | Sciencx Sunday December 26, 2021 » Neovim LSP to replace VSCode., viewed ,<https://www.scien.cx/2021/12/26/neovim-lsp-to-replace-vscode/>
VANCOUVER
Cason Adams | Sciencx - » Neovim LSP to replace VSCode. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2021/12/26/neovim-lsp-to-replace-vscode/
CHICAGO
" » Neovim LSP to replace VSCode." Cason Adams | Sciencx - Accessed . https://www.scien.cx/2021/12/26/neovim-lsp-to-replace-vscode/
IEEE
" » Neovim LSP to replace VSCode." Cason Adams | Sciencx [Online]. Available: https://www.scien.cx/2021/12/26/neovim-lsp-to-replace-vscode/. [Accessed: ]
rf:citation
» Neovim LSP to replace VSCode | Cason Adams | Sciencx | https://www.scien.cx/2021/12/26/neovim-lsp-to-replace-vscode/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.