Current NeoVim setup
post

Grumpiness and NeoVim

I have been a Vim user since the early 2000's. Someone who worked in the same building as me and was also a fellow Linux user (this was before I had bought my first Macbook) got to talking with me as I struggled to figure out Emacs suggested I sit with him for a little bit as he showed me how to use Vim. Still not sure to this day why it clicked with me but I have been using it ever since.

In the past few years I have changed which version of Vim I am using and switched to NeoVim.

Now, to be transparent, these days I use PhpStorm as my main PHP programming tool and use NeoVim for pretty much everything else. I do pay for PhpStorm because I think it's important to encourage the creation of tools for the programming languages I use. If I was still doing a lot of Python work, I'd be paying for PyCharm. I had really good experiences with it while at Mozilla.

Anyway, I still like to keep up with what is going on in the NeoVim "community" and I am happy to see a vibrant group of people creating plugins and sharing their knowledge. I wanted to give things with NeoVim and PHP another spin so it was time to go look at my current setup.

Why do all this?

My goal is to have a Vim experience that matches the way I currently work. I've been using Vim for so long that there is lots of muscle memory and I usually enable "Vim mode" in any tools I use. Editing things a "modal way" has become my default and any tools that don't support doing things that way slow me down immensely.

So, what do I want out of my NeoVim setup.

  • works well with the languages I will use
  • allows me to quickly find files
  • allows me to quickly find source definitions
  • allows me to quickly find places where code is used

Now, of course, PhpStorm does all this but also carries a lot of extra functionality around with it. Which is fine! But in an era where the tools we use on desktop operating systems get bigger and bigger and consume more and more resources, I find something appealing in using tools that take up as few resources as possible.

I did an older post on my old blog from about a year ago so I will follow the same structure but I noticed some changes. I'll be going through my current NeoVim config and filling things in as we go

set nocompatible
syntax on 
set encoding=utf8
filetype off

" Load our plugins
lua require('plugins')

I am using as much Lua as I can within NeoVim. The first few steps here are pretty much standard:

  • you always turn "no compatible" off otherwise lots of things break
  • I want syntax highlighting by default
  • I want things to be UTF8
  • I am going to define my own behaviour for how I want NeoVim to handle filetypes

Plugins

My list of plugins:

  return require('packer').startup(function()
    use 'wbthomason/packer.nvim'
    use 'neovim/nvim-lspconfig'

    -- General plugins
    use 'dracula/vim'
    use 'junegunn/vim-easy-align'
    use {
        'nvim-treesitter/nvim-treesitter',
        run = ':TSUpdate'
    }
    use 'onsails/lspkind-nvim'
    use 'vim-vdebug/vdebug'

    -- See the git status of the current line in the gutter
    use 'airblade/vim-gitgutter'

    --  PHP plugins
    use 'tpope/vim-dispatch'
    use 'StanAngeloff/php.vim'
    use 'stephpy/vim-php-cs-fixer'
    use 'jwalton512/vim-blade'
    use 'noahfrederick/vim-laravel'

    -- Help for vim-laravel
    use 'tpope/vim-projectionist'
    use 'noahfrederick/vim-composer'

    -- Respect .editorconfig files for a project
    use 'editorconfig/editorconfig-vim'

    -- Telescope support
    use 'nvim-lua/plenary.nvim'
    use 'nvim-telescope/telescope.nvim'
    use 'sharkdp/fd'
    use {'nvim-telescope/telescope-fzf-native.nvim', run = 'make' }

    -- LSP support for Typescropt
    use 'jose-elias-alvarez/nvim-lsp-ts-utils'

    -- nvim-cmp support
    use 'hrsh7th/nvim-cmp'
    use 'hrsh7th/cmp-nvim-lsp'
    use 'saadparwaiz1/cmp_luasnip'
    use 'L3MON4D3/LuaSnip'

end)

I am using Packer to handle installing all my packages. I have commented in places where I felt that things were not clear, but I guess some further explanations couldn't hurt.

  • I use the Dracula theme
  • I use vim-easy-align to make it easier to line up blocks of code
  • vim-gitgutter shows me which lines have changed from Git's perspective
  • I like to respect the EditorConfig settings for a project if they exist
  • Telescope forms the basis for a lot of fuzzy find functionality
  • I use nvim-cmp as my completion engine (and it plays nicely with Intelephense)

More NeoVim Settings

" Do smart autoindenting
set smartindent
set autoindent

" I like linenumbers, thanks
set number

" set search case to a good configuration http://vim.wikia.com/wiki/Searching
set ignorecase
set smartcase

" I like pretty colours in my terminal
set t_Co=256

" Let's get some good colours in our terminal
let $NVM_TUI_ENABLE_TRUE_COLOR=1
set termguicolors
color dracula 

" We want to use ripgrep for any grep commands
set grepprg='rg'

" Basic configuration options
set tabstop=4
set shiftwidth=4
set softtabstop=0
set smarttab
set expandtab
set wildmenu
set wildmode=list:longest,full
set ttyfast
set showmatch
set hlsearch
set incsearch
set backspace=indent,eol,start

" Make sure we are using the version of Python we want
let g:python3_host_prog = "/opt/homebrew/bin/python3"

" We always want to use UTF-8
set encoding=UTF-8
set fileencoding=UTF-8

A lot of what is up there is fairly straightforward when it comes to Vim/NeoVim, so I am not going to go over a lot of them.

LSP Configuration

This is the critical piece for me -- supporting different languages makes NeoVim so versatile.

In my config I have these two lines:

lua require('lsp-config')
lua require('nvm-cmp')

and these handle my languages and making sure autocompletion behaves as I expect.

--- Configuration for LSP, formatters, and linters.
local nvim_lsp = require("lspconfig")

-- short cut methods.
local t = function(str)
  return vim.api.nvim_replace_termcodes(str, true, true, true)
end

local opts = { noremap=true, silent=true }
vim.api.nvim_set_keymap('n', '<space>e', '<cmd>lua vim.diagnostic.open_float()<CR>', opts)
vim.api.nvim_set_keymap('n', '[d', '<cmd>lua vim.diagnostic.goto_prev()<CR>', opts)
vim.api.nvim_set_keymap('n', ']d', '<cmd>lua vim.diagnostic.goto_next()<CR>', opts)
vim.api.nvim_set_keymap('n', '<space>q', '<cmd>lua vim.diagnostic.setloclist()<CR>', opts)
vim.api.nvim_set_keymap('n', '<space>f', '<cmd>lua vim.lsp.buf.formatting()<CR>', opts)

local on_attach = function(client, bufnr)
  -- Enable completion triggered by <c-x><c-o>
  vim.api.nvim_buf_set_option(bufnr, 'omnifunc', 'v:lua.vim.lsp.omnifunc')

  -- Mappings.
  -- See `:help vim.lsp.*` for documentation on any of the below functions
  vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gD', '<cmd>lua vim.lsp.buf.declaration()<CR>', opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gd', '<cmd>lua vim.lsp.buf.definition()<CR>', opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', 'K', '<cmd>lua vim.lsp.buf.hover()<CR>', opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gi', '<cmd>lua vim.lsp.buf.implementation()<CR>', opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', '<C-k>', '<cmd>lua vim.lsp.buf.signature_help()<CR>', opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', '<space>wa', '<cmd>lua vim.lsp.buf.add_workspace_folder()<CR>', opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', '<space>wr', '<cmd>lua vim.lsp.buf.remove_workspace_folder()<CR>', opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', '<space>wl', '<cmd>lua print(vim.inspect(vim.lsp.buf.list_workspace_folders()))<CR>', opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', '<space>D', '<cmd>lua vim.lsp.buf.type_definition()<CR>', opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', '<space>rn', '<cmd>lua vim.lsp.buf.rename()<CR>', opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', '<space>ca', '<cmd>lua vim.lsp.buf.code_action()<CR>', opts)
  vim.api.nvim_buf_set_keymap(bufnr, 'n', 'gr', '<cmd>lua vim.lsp.buf.references()<CR>', opts)
end

-- PHP
nvim_lsp.intelephense.setup {
    cmd = { "intelephense", "--stdio" },
    filetypes = { "php" },
}

--- Linter setup
local filetypes = {
  typescript = "eslint",
  typescriptreact = "eslint",
  php = {"phpcs", "psalm"},
}

local linters = {
  phpcs = {
    command = "vendor/bin/phpcs",
    sourceName = "phpcs",
    debounce = 300,
    rootPatterns = {"composer.lock", "vendor", ".git"},
    args = {"--report=emacs", "-s", "-"},
    offsetLine = 0,
    offsetColumn = 0,
    sourceName = "phpcs",
    formatLines = 1,
    formatPattern = {
      "^.*:(\\d+):(\\d+):\\s+(.*)\\s+-\\s+(.*)(\\r|\\n)*$",
      {
        line = 1,
        column = 2,
        message = 4,
        security = 3
      }
    },
    securities = {
      error = "error",
      warning = "warning",
    },
    requiredFiles = {"vendor/bin/phpcs"}
  },
  psalm = {
    command = "./vendor/bin/psalm",
    sourceName = "psalm",
    debounce = 100,
    rootPatterns = {"composer.lock", "vendor", ".git"},
    args = {"--output-format=emacs", "--no-progress"},
    offsetLine = 0,
    offsetColumn = 0,
    sourceName = "psalm",
    formatLines = 1,
    formatPattern = {
      "^[^ =]+ =(\\d+) =(\\d+) =(.*)\\s-\\s(.*)(\\r|\\n)*$",
      {
        line = 1,
        column = 2,
        message = 4,
        security = 3
      }
    },
    securities = {
      error = "error",
      warning = "warning"
    },
    requiredFiles = {"vendor/bin/psalm"}
  }
}

nvim_lsp.diagnosticls.setup {
  on_attach = on_attach,
  filetypes = vim.tbl_keys(filetypes),
  init_options = {
    filetypes = filetypes,
    linters = linters,
  },
}

A lot of what is in here I simple stole from other people's configurations but I think most of it should be straightforward to figure out.

Some highlights from my perspective:

  • you pick which language servers to care about
  • you map functionality to existing Vim bindings so, again, it behaves as expected

Here is what I have for getting the autocompletion engine working:

local capabilities = vim.lsp.protocol.make_client_capabilities()

local lspconfig = require('lspconfig')

-- Enable some language servers with the additional completion capabilities offered by nvim-cmp
local servers = { 'intelephense', 'tsserver' }
for _, lsp in ipairs(servers) do
  lspconfig[lsp].setup {
    -- on_attach = my_custom_on_attach,
    capabilities = capabilities,
  }
end

-- luasnip setup
local luasnip = require 'luasnip'

-- nvim-cmp setup
local cmp = require 'cmp'
cmp.setup {
  snippet = {
    expand = function(args)
      require('luasnip').lsp_expand(args.body)
    end,
  },
  mapping = {
    ['<C-p>'] = cmp.mapping.select_prev_item(),
    ['<C-n>'] = cmp.mapping.select_next_item(),
    ['<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 {
      behavior = cmp.ConfirmBehavior.Replace,
      select = true,
    },
    ['<Tab>'] = function(fallback)
      if cmp.visible() then
        cmp.select_next_item()
      elseif luasnip.expand_or_jumpable() then
        luasnip.expand_or_jump()
      else
        fallback()
      end
    end,
    ['<S-Tab>'] = function(fallback)
      if cmp.visible() then
        cmp.select_prev_item()
      elseif luasnip.jumpable(-1) then
        luasnip.jump(-1)
      else
        fallback()
      end
    end,
  },
  sources = {
    { name = 'nvim_lsp' },
    { name = 'luasnip' },
  },
}

Again, more mapping of existing keys to get the plugin to behave as expected. This sort of thing lies at the very heart of how Vim/NeoVim plugins do so much work -- they literally alter how the application behaves by overriding things. Perhaps this is actually a form of monkey patching? TIME IS A CIRCLE.

Key Mappings

" ------------------------------------------------------------------------------
" # Mappings
" ------------------------------------------------------------------------------
" # All of your mappings go in this file! Don't worry about your mappings
" # being separate from related config. Sourcery provides mappings to
" # easily jump between plugin definitions, mappings, and configs.
" #
" # More info: https://github.com/jesseleite/vim-sourcery#jumping-between-files


" ------------------------------------------------------------------------------
" # Example
" ------------------------------------------------------------------------------

" easily switch between vsplit windows
map <Leader>j <C-w>j
map <Leader>k <C-w>k
map <Leader>h <c-w>h
map <Leader>l <c-w>l

" Remove highlighing of search terms
nnoremap <leader><space> :nohlsearch<CR>

" Mappings for EasyAlign
xmap ga <Plug>(EasyAlign)
nmap ga <Plug>(EasyAlign)

" Use <Tab> and <S-Tab> to navigate through popup menu
inoremap <expr> <Tab>   pumvisible() ? "\<C-n>" : "\<Tab>"
inoremap <expr> <S-Tab> pumvisible() ? "\<C-p>" : "\<S-Tab>"

" Telescope Lua mappings
nnoremap <leader>ff <cmd>lua require('telescope.builtin').find_files()<cr>
nnoremap <leader>fg <cmd>lua require('telescope.builtin').live_grep()<cr>
nnoremap <leader>fb <cmd>lua require('telescope.builtin').buffers()<cr>
nnoremap <leader>fh <cmd>lua require('telescope.builtin').help_tags()<cr>
nnoremap <leader>fr <cmd>lua require('telescope.builtin').lsp_references()<cr>
nnoremap <leader>fd <cmd>lua require('telescope.builtin').lsp_definitions()<cr>
nnoremap <leader>ft <cmd>lua require('telescope.builtin').lsp_type_definitions()<cr>

No Vim setup is complete without mapping and re-mapping keys in Vim. The list above mostly focuses on making Telescope friendlier to use.

So there you have it -- this is my current NeoVim setup. It is more than sufficient for me to do daily PHP development work. What are some other things I am looking to integrate into my setup?

  • Make it easier to use XDebug
  • More refactoring tools (I understand Intelephense can help but I have also experimented with Phpactor

As always, I continue to tweak my configuration as I evaluate new tools or discover new ways of completing old tasks. I hope you find my setup useful.

Categories: tools