Neovim LSP to replace VSCode
Tags: vim, vscode, tutorial, productivity
Published: 2021-12-26 20:26:48

If you are looking for a much simpler solution to replacing VSCode with a terminal editor I suggest reading my [minimal vim](https://dev.to/casonadams/minimal-vim-with-ide-like-setup-10jf) post.

## TLDR;

All the code for this is availible [nvim-code](https://github.com/casonadams/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.

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

## init.lua

```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](https://www.lunarvim.org/configuration/01-settings.html).

```lua
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.

```lua
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

```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

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

## _telescope.lua

```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

```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

```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.

```py
#!/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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/64ckuhr3xrp6z68lqkm1.png)

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 
w to use `black` as the python formatter.  We are using [null-ls](https://github.com/jose-elias-alvarez/null-ls.nvim/blob/main/doc/BUILTINS.md) for the formatting here.

`after/ftplugin/python.lua`

```lua 
 local null_ls = require("null-ls")

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

null_ls.setup({ sources = sources })
```

**Note** null_ls can setup all settings in a regular way like the other lua files we created. Using the after dir / lazy loading is just one way of doing this.

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`.

```py
#!/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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h06jshojrppytmvvka8x.png)

And that about wraps it up.