My Neovim Setup: From Zero to Productive

Table of Contents

My Neovim Setup: From Zero to Productive

I recently decided to build my own Neovim configuration from scratch instead of using a pre-built distribution like LazyVim or NVChad. Here’s what I learned and how you can set up a similar environment for JavaScript/TypeScript development.

Why Build Your Own Config?

Pre-built configurations are great, but building your own teaches you: - How Neovim actually works under the hood

  • How to debug issues when plugins break
  • Exactly what each keybinding does and why

Plus, you end up with a minimal setup that only includes what you actually need.

The Plugins

My setup uses Packer as the plugin manager. Here’s what I’m running:

Plugin Purpose
onedark.nvim Beautiful dark theme
lualine.nvim Status line
nvim-tree.lua File explorer
telescope.nvim Fuzzy finder (game changer!)
nvim-treesitter Better syntax highlighting
nvim-lspconfig Language server support
nvim-cmp Autocompletion
kulala.nvim HTTP client for API testing

Key Settings

vim.opt.clipboard = 'unnamedplus' -- System clipboard integration
vim.opt.number = true             -- Line numbers
vim.opt.relativenumber = true     -- Relative line numbers
vim.opt.cursorline = true         -- Highlight current line
vim.opt.expandtab = true          -- Spaces instead of tabs
vim.opt.shiftwidth = 2            -- 2-space indentation
vim.opt.tabstop = 2
vim.g.mapleader = ' '             -- Space as leader key

The relative line numbers are crucial for Vim efficiency - they let you quickly jump to any visible line with commands like 5j or 12k.

The clipboard integration means everything you yank in Neovim goes straight to your system clipboard. Copy in Neovim, paste in your browser. No more "+y gymnastics.

My Keyboard Shortcuts

File Navigation

Shortcut Action
Space + e Toggle file tree
Space + f Focus file tree
Space + ff Find files (fuzzy search)
Space + fg Live grep (search text in project)
Space + fb Find open buffers

Telescope is the real productivity booster here. Instead of navigating through folders, I just hit Space + ff, type a few characters of the filename, and I’m there.

Tab Management

Shortcut Action
Space + tn New tab
Space + tc Close tab
Tab Next tab
Shift + Tab Previous tab

Code Intelligence (LSP)

Shortcut Action
gd Go to definition
gr Find references
K Hover documentation
Space + rn Rename symbol
Space + ca Code actions

Autocompletion

Shortcut Action
Ctrl + Space Trigger completion
Tab / Shift + Tab Navigate suggestions
Enter Confirm selection
Ctrl + e Close menu

HTTP Client (Kulala)

Shortcut Action
Space + hr Run HTTP request
Space + ha Run all requests
Space + hn / Space + hp Navigate between requests
Space + hy Copy as cURL

Create a .http file like this:

### Get users
GET https://api.example.com/users

### Create user
POST https://api.example.com/users
Content-Type: application/json

{
  "name": "John",
  "email": "john@example.com"
}

Then just press Space + hr on any request to run it. No need to switch to Postman!

Clipboard Operations

Shortcut Action
yy Copy line
yw Copy word
y (visual) Copy selection
:%y Copy entire file
dd Cut line
p Paste

Essential Vim Motions to Master

The real power of Vim comes from combining operators with motions. Here’s the formula:

operator + motion = action

Operators

  • d - delete
  • c - change (delete + insert mode)
  • y - yank (copy)
  • > / < - indent

Text Objects

Text objects let you operate on semantic units of text:

Command What it does
ciw Change inner word
ci" Change inside quotes
ci( Change inside parentheses
di{ Delete inside braces
da" Delete around quotes (including quotes)
yap Yank a paragraph

My Favorites

Command What it does
ciw Change the word under cursor
ci" Change text inside quotes
dt) Delete until closing parenthesis
f= Jump to next = sign
. Repeat last change
* Search for word under cursor
:%s/old/new/g Replace all in file

The Practice File

To really internalize these shortcuts, I created a comprehensive practice file. It has 12 sections covering:

  1. Horizontal Movement - w, b, f, t, 0, $
  2. Vertical Movement - j, k, {, }, gg, G
  3. Text Objects - ciw, ci", di(, da{
  4. Operators + Motions - Combining d, c, y with movements
  5. Visual Mode - v, V, Ctrl+v
  6. Dot Command - Using . to repeat actions
  7. Search & Replace - /, *, :%s
  8. Macros - q, @
  9. Registers - Named registers for multiple clipboards
  10. Marks - Bookmarking positions in files
  11. Window Splits - Managing multiple views
  12. Real World Challenges - Practical refactoring exercises

Here’s a sample exercise:

// TASK: Change "oldValue" to "newValue" (use f then ciw)
const result = calculate(oldValue, multiplier, offset)

// TASK: Delete everything inside parentheses (use di()
processData(argument1, argument2, argument3, somethingElse, moreStuff)

// TASK: Use Ctrl+v (block visual) to add "//" before each line
console.log("comment me 1")
console.log("comment me 2")
console.log("comment me 3")

The full practice file is available in my config repo. I recommend going through it section by section, trying to complete each task with the fewest keystrokes possible.

Tips for Learning

  1. Start with the basics - Master hjkl, w, b, e before moving to complex motions
  2. Learn one new thing per day - Don’t try to memorize everything at once
  3. Use the dot command - . repeats your last change, making repetitive edits trivial
  4. Think in text objects - “Change inside quotes” (ci") is faster than selecting text manually
  5. Practice on real code - The practice file helps, but real projects teach you faster

Conclusion

Building my own Neovim config took some time, but now I understand exactly what’s happening and can fix issues when they arise. The combination of Telescope for navigation, LSP for code intelligence, and mastering Vim motions has made me significantly faster at editing code.

The Full Configuration

Here’s my complete init.lua for reference:

-- Initialize packer (plugin manager)
vim.cmd [[packadd packer.nvim]]

require('packer').startup(function()
  use 'wbthomason/packer.nvim' -- Packer manages itself
  use 'nvim-tree/nvim-web-devicons' -- Icons for file tree and statusline

  -- Aesthetic Theme
  use {
    'navarasu/onedark.nvim',
    config = function()
      require('onedark').setup {
        style = 'darker' -- Options: 'dark', 'darker', 'cool', 'deep', 'warm', 'warmer', 'light'
      }
      require('onedark').load()
    end
  }

  -- Statusline
  use {
    'nvim-lualine/lualine.nvim',
    requires = { 'nvim-tree/nvim-web-devicons' },
    config = function()
      require('lualine').setup {
        options = {
          theme = 'onedark'
        }
      }
    end
  }

 -- File Tree Viewer
  use {
    'nvim-tree/nvim-tree.lua',
    requires = { 'nvim-tree/nvim-web-devicons' }, -- Optional icons
    config = function()
      require('nvim-tree').setup {
  view = {
    width = 30, -- Width of the tree viewer
    side = 'left', -- Open on the left
  },
  filters = {
    dotfiles = false, -- Show dotfiles (.env, .gitignore, etc.)
  }
      }
    end
  }

  -- Telescope (fuzzy finder)
  use {
    'nvim-telescope/telescope.nvim',
    tag = '0.1.8',
    requires = { 'nvim-lua/plenary.nvim' },
  }

  -- Treesitter (better syntax highlighting)
  use {
    'nvim-treesitter/nvim-treesitter',
    run = ':TSUpdate',
  }

  -- LSP Support
  use 'neovim/nvim-lspconfig'

  -- Autocompletion
  use 'hrsh7th/nvim-cmp'
  use 'hrsh7th/cmp-nvim-lsp'
  use 'hrsh7th/cmp-buffer'
  use 'hrsh7th/cmp-path'
  use 'L3MON4D3/LuaSnip'
  use 'saadparwaiz1/cmp_luasnip'

  -- Kulala (HTTP client)
  use {
    'mistweaverco/kulala.nvim',
    requires = { 'nvim-lua/plenary.nvim' },
    ft = { 'http' },
    config = function()
      vim.cmd('silent! lua require("kulala").setup()')

      local kulala = require('kulala')
      vim.keymap.set('n', '<leader>hr', kulala.run, { buffer = true, desc = 'Run HTTP request' })
      vim.keymap.set('n', '<leader>ha', kulala.run_all, { buffer = true, desc = 'Run all HTTP requests' })
      vim.keymap.set('n', '<leader>hp', kulala.jump_prev, { buffer = true, desc = 'Previous HTTP request' })
      vim.keymap.set('n', '<leader>hn', kulala.jump_next, { buffer = true, desc = 'Next HTTP request' })
      vim.keymap.set('n', '<leader>hy', kulala.copy, { buffer = true, desc = 'Copy request as cURL' })
      vim.keymap.set('n', '<leader>ht', kulala.toggle_view, { buffer = true, desc = 'Toggle headers/body' })
    end,
  }

end)


-- Basic Settings
vim.opt.clipboard = 'unnamedplus' -- Use system clipboard
vim.opt.number = true        -- Show line numbers
vim.opt.relativenumber = true -- Relative line numbers
vim.opt.cursorline = true    -- Highlight cursor line
vim.opt.termguicolors = true -- Enable true colors
vim.opt.wrap = false         -- Disable line wrapping
vim.opt.expandtab = true     -- Use spaces instead of tabs
vim.opt.shiftwidth = 2       -- Number of spaces for each indentation
vim.opt.tabstop = 2          -- Number of spaces for a tab
vim.opt.smartindent = true   -- Enable smart indentation

-- Changing the mapleader
vim.g.mapleader = ' ' -- Set leader to spacebar

-- Toggle file tree with <leader>e
vim.api.nvim_set_keymap('n', '<leader>e', ':NvimTreeToggle<CR>', { noremap = true, silent = true })

-- Focus file tree with <leader>f
vim.keymap.set('n', '<leader>f', ':NvimTreeFocus<CR>', { desc = 'Focus file tree' })

-- Tab navigation (auto-open nvim-tree in new tabs)
vim.keymap.set('n', '<leader>tn', ':tabnew<CR>:NvimTreeOpen<CR>', { desc = 'New tab' })
vim.keymap.set('n', '<leader>tc', ':tabclose<CR>', { desc = 'Close tab' })
vim.keymap.set('n', '<Tab>', ':tabnext<CR>', { desc = 'Next tab' })
vim.keymap.set('n', '<S-Tab>', ':tabprevious<CR>', { desc = 'Previous tab' })

-- Telescope keymaps
local telescope_ok, telescope_builtin = pcall(require, 'telescope.builtin')
if telescope_ok then
  vim.keymap.set('n', '<leader>ff', telescope_builtin.find_files, { desc = 'Find files' })
  vim.keymap.set('n', '<leader>fg', telescope_builtin.live_grep, { desc = 'Live grep' })
  vim.keymap.set('n', '<leader>fb', telescope_builtin.buffers, { desc = 'Find buffers' })
  vim.keymap.set('n', '<leader>fh', telescope_builtin.help_tags, { desc = 'Help tags' })
end

-- Open nvim-tree on startup
vim.api.nvim_create_autocmd('VimEnter', {
  callback = function()
    require('nvim-tree.api').tree.open()
  end
})

-- Enable filetype plugins
vim.cmd('filetype plugin on')

-- LSP Configuration (Neovim 0.11+ native API)
local ok_cmp_lsp, cmp_nvim_lsp = pcall(require, 'cmp_nvim_lsp')

-- JavaScript/TypeScript/JSX/React support
vim.lsp.config.ts_ls = {
  cmd = { 'typescript-language-server', '--stdio' },
  filetypes = { 'javascript', 'javascriptreact', 'javascript.jsx', 'typescript', 'typescriptreact', 'typescript.tsx' },
  root_markers = { 'package.json', 'tsconfig.json', 'jsconfig.json', '.git' },
  capabilities = ok_cmp_lsp and cmp_nvim_lsp.default_capabilities() or nil,
}
vim.lsp.enable('ts_ls')

-- LSP Keybindings (when LSP attaches to a buffer)
vim.api.nvim_create_autocmd('LspAttach', {
  callback = function(args)
    local opts = { buffer = args.buf }
    vim.keymap.set('n', 'gd', vim.lsp.buf.definition, opts)        -- Go to definition
    vim.keymap.set('n', 'K', vim.lsp.buf.hover, opts)              -- Hover docs
    vim.keymap.set('n', 'gr', vim.lsp.buf.references, opts)        -- Find references
    vim.keymap.set('n', '<leader>rn', vim.lsp.buf.rename, opts)    -- Rename symbol
    vim.keymap.set('n', '<leader>ca', vim.lsp.buf.code_action, opts) -- Code actions
  end,
})

-- Autocompletion Setup (only load if plugins are installed)
local ok_cmp, cmp = pcall(require, 'cmp')
local ok_luasnip, luasnip = pcall(require, 'luasnip')

if ok_cmp and ok_luasnip then
  cmp.setup {
    snippet = {
      expand = function(args)
        luasnip.lsp_expand(args.body)
      end,
    },
    mapping = cmp.mapping.preset.insert({
      ['<C-b>'] = cmp.mapping.scroll_docs(-4),
      ['<C-f>'] = cmp.mapping.scroll_docs(4),
      ['<C-Space>'] = cmp.mapping.complete(), ['<C-e>'] = cmp.mapping.abort(),
      ['<CR>'] = cmp.mapping.confirm({ select = true }),  -- Enter to confirm
      ['<Tab>'] = cmp.mapping(function(fallback)          -- Tab to navigate
        if cmp.visible() then
          cmp.select_next_item()
        elseif luasnip.expand_or_jumpable() then
          luasnip.expand_or_jump()
        else
          fallback()
        end
      end, { 'i', 's' }),
      ['<S-Tab>'] = cmp.mapping(function(fallback)
        if cmp.visible() then
          cmp.select_prev_item()
        elseif luasnip.jumpable(-1) then
          luasnip.jump(-1)
        else
          fallback()
        end
      end, { 'i', 's' }),
    }),
    sources = cmp.config.sources({
      { name = 'nvim_lsp' },  -- LSP completions
      { name = 'luasnip' },   -- Snippets
      { name = 'buffer' },    -- Words from current buffer
      { name = 'path' },      -- File paths
    }),
  }
end

-- Telescope Setup (only load if plugin is installed)
local ok_telescope, telescope = pcall(require, 'telescope')
if ok_telescope then
  telescope.setup {
    defaults = {
      preview = {
        treesitter = false,
      },
    }
  }
end

-- Treesitter Setup (only load if plugin is installed)
local ok_ts, ts_configs = pcall(require, 'nvim-treesitter.configs')
if ok_ts then
  ts_configs.setup {
    ensure_installed = { 'javascript', 'typescript', 'tsx', 'html', 'css', 'json', 'lua' },
    ignore_install = { 'kulala_http' },
    auto_install = false,
    highlight = { enable = true },
    indent = { enable = true },
  }
end

comments powered by Disqus