Decluttering My IDE Setup
Decluttering my IDE
So I’ve been on vim for good chunk of study and professional career. First tried it out around 2011. Started with original vim, not sure if neovim was around back then, so it’s been not very IDE like. But concept of motions stuck with me so I used it ever since. I took a pause on vim in 2016 when working with a lot of Java and Scala code. JetBrains just had an edge in pretty much everything except performance/hardware requirements. I was a happy user of Intellij with Vim bindings for a while but the plugin, while offered basic motions, did not work well with things like setting jumps and I missed that functionality dearly. I never fully quit Vim, using as a text editor for most of my work in terminal but my development workflow shifted to Intellij for JVM languages.
Around 2021/2022 I tried out VSCode and while I liked simplicity and extensibility compared to Visual Studio and Intellij, still the concept that Electron uses to effectively run web browser as a desktop app is somewhat lost of me. Then there’s packaging that web browser in every app inflating it - not very inspiring as someone passionate about technology. And their vim bindings were lacking as well.
Then Crowdstrike happened, not the outage but the company I worked for at the time decided their security standards were lacking and they absolutely must install Crowdstrike daemons on every engineer’s laptop to scan all their files, including build caches. It turned out too much for my MacBook 2019 with 12-core processor so every time I fired up Intellij to work on a new change it was a very painful experience and that’s how I ended up trying to use Neovim as my main IDE. The Crowdstrike daemon has been optimized since but this setup stuck with me and given all the improvements with LSP support I don’t really feel like I’m missing anything anymore.
If you want a pre-built somewhat opinionated IDE experience with neovim you can bootstrap yourself with a number of “plugin collections” or “flavours”. I started with NvChad and it was pleasant experience overall especially cheat-sheet for shortcuts you can toggle with <leader>ch, built-in LSP support for linting and diagnostics, plugin for browsing file tree etc. But it always scratched me in a wrong place to have some custom code that NvChad is providing out of the box. But I had to ship a few things so I shelved that feeling for a while. Until it started breaking, NvChad decided their config structure was not “optimal” anymore and changed it entirely, this in turn broke my LSP configuration so instead of working IDE I got an unhappy Neovim that is throwing errors and deprecation warnings at me. I spent a bit of time patching stuff, doing clean re-installs but it got me thinking that maybe simplicity I get from just getting a ready-made config is requiring higher maintenance that making this configuration from the clean slate, there are only a handful plugins that I really need so how hard could that be? So after a long intro this is what this post is about.
Neovim configuration entry-point is ~/.config/nvim/init.lua file (I’m assuming you are on unix: macos, linux) and it is a plain Lua file that will be evaluated line-by-line every time you open the editor.
Plugin manager: Lazy
First things first, we need a way to install plugins, NvChad bundles Lazy and I don’t have any problem with this, LLM suggested this code to bootstrap Lazy if it is not installed yet.
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", lazypath,
})
end
vim.opt.rtp:prepend(lazypath)vim object is a built-in that is provided when we evaluate init.lua config and that’s how we interact with the editor, set options, call functions like fn.system to execute shell commands.
Once Lazy is set up we can actually install some plugins with following snippet.
require('lazy').setup({
{'plugin1', opts = {}},
{
'plugin1',
config = function()
end
}
})Some plugins use opts argument to set configurations, some accept a callable function as config argument, usually this is in plugin documentation along with defaults.
Telescope
Probably most important plugin for me, Telescope provides search, you can search files, buffers, definitions provided by LSP, Neovim commands, pretty much anything. Telescope calls those search providers pickers and you can customize how those pickers show their results, sort them etc. etc.
If you are coming from Intellij you would feel right at home with Telescope, those <Cmd-S-A> (search IDE actions), <Cmd-S-F> (search files) command translate to corresponding pickers and you can bind them to the same keymaps if you want to, later more on that.
You can customize each output window quite heavily but to get started Telescope provides themes for commonly used layouts. For example I use dropdown for files and open buffers.
require('telescope').setup({
pickers = {
find_files = { theme = 'dropdown', previewer = false },
buffers = { theme = 'dropdown', previewer = false },
}
})
For symbols I like to have a separate panel similar to how Intellij does it and this can be achieved with layout.
layout_config = {
anchor = "E",
width = 0.45,
height = 0.99,
prompt_position = "top",
},Here we specify a panel that would take roughly right half of the screen, anchor specifies where to attach it and prompt_position is where the search bar is.

It is not maybe as pretty as what Intellij does but it does the job of showing the file outline.
Lua-Line
Have you seen those beautiful status lines by NvChad, LunarVim? I feel they sometimes are a bit over the top, like having a git branch, file type, etc. I usually know which branch I’m working on and don’t need a reminder of what PL I should be writing. Instead of having this extra bells and whistles I opted to show open buffers as a way to simulate tabs. I also bound <TAB>/<S-TAB> to cycle between them in normal mode.

I also set it to be global as I don’t need a status in each split. I’ve also added initiated commands (%S) on the right for visual feedback typing motions that summon Telescope windows or other keymaps.
Mason & LSP-config
This is to get an actual IDE features, go-to-definition, linting, diagnostics. Mason is a plugin that is used for managing different language integrations, like pyright or rust-analyzer. It installs, updates, and deletes when those are no longer needed. LSP-config plugin provides actual language server integration, e.g. firing it up when open file language is matching one of installed backend, as well as integrated with the editor diagnostics (errors / warnings you see in open buffer if any).
I decided to have those as a single config using dependencies parameter in Lazy, so it looks like this.
{
"neovim/nvim-lspconfig",
event = {"BufReadPost", "BufNewFile"},
dependencies = {
{ "williamboman/mason.nvim", config = true },
{ "williamboman/mason-lspconfig.nvim", config = true, ensure_installed = {'lua_ls', 'basedpyright', 'rust_analyzer'} },
},
config = function ()
vim.lsp.enable('lua_ls')
vim.lsp.enable('basedpyright')
vim.lsp.enable('rust_analyzer')
end
},Important note here, this plugin is loading on event this is to prevent it triggering for text files to speedup startup. Also as you see list of language servers can be factored out as a variable then config section can be updated as a for loop - benefits of using actual PL for configuration.
Vim-cmp & Autopairs
To provide truly IDE-like experience I use Vim Completion which can show suggestion from various sources, including LSP, buffer items, file-system. I set it up to fire on typing in insert mode, this way it mimics how Intellij handles it. Depending on LSP backend you can get some nice bits like auto-imports (provided by basedpyright but missing in pyright).
mapping = cmp.mapping.preset.insert({
['<TAB>'] = cmp.mapping.confirm({ select = true }),
}),
sources = cmp.config.sources({
{ name = 'nvim_lsp' },
{ name = 'buffer' },
{ name = 'path' },
}),Here <TAB> will accept top suggestion. This is slightly different from what NvChad does, where you can cycle suggestions with <TAB>/<S-TAB>, then accept with enter (<CR>). In my usage too many times I want to go to newline and get suggestion accepted instead. Tab-completion is how Intellij is setting it and I can change topmost suggestion by simply typing more symbols as those are matched with fuzzy search.

Notes on startup speed
When testing out my config I found that it takes sometimes up to 1s to load so I was a bit puzzled as I don’t have as much stuff as NvChad bundles yet it is significantly slower. Even more puzzling was that it only happened with python sources and lua, rust or anything else was fairly snappy.
You can get launch profile for neovim when providing startuptime argument with path to output log file.
neovim --startuptime startup.log main.pyTurned out neovim has a number of built-in language providers and python is one of the included languages, so it tries to find system interpreter which for some reason took ages. I did not spend much time to debug this, but since we use LSP in this setup anyway, we don’t really need this so I’ve disabled it, now loading times are much more comparable to optimized startup of NvChad.
vim.g.loaded_python3_provider = 0Another note is as init.lua is evaluated line-by-line on each startup, every require('plugin') call you have in the file will cause eager start of that plugin, so even if you use Lazy to delay plugin startup it will still be loaded, so it is best to avoid those extra calls, only having top-level call to Lazy.
Conclusion
All-in-all I’m quite happy with this setup, I occasionally experiment with adding new plugins but these provide main chunk of functionality. You can find complete config here alongside some instructions how to get started.