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.
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 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 })
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
.
#!/usr/bin/env python
def echo(msg):
print(msg)
if __name__ == "__main__":
echo("Hello World!")
- Note
leader
isspace
Now to tell nvim
to format with the defined formatter send leader lf
.
And the result should be a nicely formatted python
file.
And that about wraps it up.
Top comments (18)
It would be great adding a safe call at each plugin setting like this:
Just in case the call fail, we just get a return without trying to read the rest of config file, the same for the packer config:
By the way
pcall
it is, if I am not wrong, the same as atry catch
statement.Great suggestion. I updated the git repo. Not sure if I should update this post or not. I am thinking no. And hopefully most people just go look at the source repo.
I always add an update to my posts. A lot of new devs rely on good blogs to get started.
Why?
Why replace vscode?
I work in a terminal.
@casonadams
Great article.
However, I find it difficult to understand,
vscode
hasRemote-SSH
extension and you can connect vscode to your server.My personal experience has been 'its difficult to maintain huge nvim/vim configs over a period of time'. And instead of focusing on my actual work, I end up in configure-reconfigure vim/nvim configs most of the time.
Since I am very comfortable with vim, I use
vscode-vim
all the time.Thanks! It's not that vscode is bad and yes there are some other amazing features like the remote connect offering, and live share.
For me I use nvim as my daily driver because I work in a terminal all day. And I find it fun to tweak things here and there as needed.
I do follow your word of caution that one can go down a hole of wasted time with config tweaking. And the whole point of learning vim, is that's it's installed on more servers. So if you need to hop on a machine and check it out you can. But that defeats the purpose of this article, because once you configure vim this way, it's not vanilla any more.
Thanks for your comment.
Agreed.
I am a big fan of vim/nvim and I am using vim for 10 years.
Recently, when I looked back, I was (wasting) a lot of my time in trying out new configs just for the fun of it.
And decided to stick to vscode as the main editor, ofcourse with
vscode-vim
extension :-)So I tinkered with vscode for a bit, and there is just as much config with it as there is vim. With less flexibility. Going to stick with vim. Thanks for the thoughts though.
@casonadams I would love to see your work in your vim config.
Have you made any videos?
I also have a very simple and nice configuration check it out here.
github.com/abdoulayegk/dotefiles
Here is how it looks.
To install it just follow the instructions given in the above link or just download it and put the init.vim file inside the is file ~/.config/nvim then open nvim it will install everything for you.
Enjoy 😊🤩
Looks like you are using coc and viml. I have a similar (much simpler) example in this post
This post is very specific to getting up and running with
neovim
and the built-in LSP. Including some nice to haves to walk away from VSCode, and live in a terminal editor.Hello everyone!
@casonadams in
_lsp.lua
on line 93We have this
capabilities = require("cmp_nvim_lsp").update_capabilities(vim.lsp.protocol.make_client_capabilities()),
Today we need swap to
capabilities = require("cmp_nvim_lsp").dafault_capabilities(vim.lsp.protocol.make_client_capabilities()),
Because
update_capabilities
is now deprecatedThat is it and thank you for great article!
It'll probably take hours for me to translate my vimscript configuration to lua, so I want to hear if there would be any advantages in doing so.
Haha, yeah this post is my hours of converting my viml to lua. I'm not sure on the bang for the buck yet.
But using lua moving forward might open more doors.
I think if one were wanting to start using the built-in lsp stuff it will be worth the effort.
Speed. Lua is faster than vimscript
Yessss. Let's go Vim!