When I started working in TypeScript and React, I found it challenging to continue using vim. I switched to Visual Studio Code because it was better suited for the task at hand. But what made it better boiled down to the language server integration which provided all the common code actions you would expect from an IDE such as automatic importing, symbol renaming, tool tip display of compiler and linter errors, and go-to type/definition/reference. I still missed the text editing power of vim.
The question became: could I have the best of both worlds and get all those features (and more) in vim?
It all starts with the syntax
For the basics, I use pangloss/vim-javascript for JavaScript syntax, and leafgarland/typescript-vim. There are other options worth exploring, but these have served me well out of the box and are fairly configurable.
For working with JSX I use MaxMEllon/vim-jsx-pretty, and for TSX I use peitalin/vim-jsx-typescript. vim-jsx-pretty
does have TypeScript support, but I had some performance issues when highlighting large TSX files.
For projects that use styled components, I use styled-components/vim-styled-components which highlights CSS inside the styled
and css
template strings.
Similarly, for projects that use [GraphQL], jparise/vim-graphql has been great for highlighting queries in gql
template strings.
I install my plugins using junegunn/vim-plug:
Plug 'pangloss/vim-javascript'
Plug 'leafgarland/typescript-vim'
Plug 'peitalin/vim-jsx-typescript'
Plug 'styled-components/vim-styled-components', { 'branch': 'main' }
Plug 'jparise/vim-graphql'
Highlighting for large files
Sometimes syntax highlighting can get out of sync in large JSX and TSX files. This was happening too often for me so I opted to enable syntax sync fromstart
, which forces vim to rescan the entire buffer when highlighting. This does so at a performance cost, especially for large files. It is significantly faster in Neovim than in vim.
I prefer to enable this when I enter a JavaScript or TypeScript buffer, and disable it when I leave:
autocmd BufEnter *.{js,jsx,ts,tsx} :syntax sync fromstart
autocmd BufLeave *.{js,jsx,ts,tsx} :syntax sync clear
Going above and beyond with Coc
With a solid set of syntax highlighting in place, next up is to integrate the TypeScript language server. All the heavy lifting here is done by Conquer of Completion – a language server plugin for Neovim (and vim)! There are other great alternatives to Coc (most notably ale), but I prefer Coc for a couple of reasons:
- It makes my vim feel like an IDE (auto-import, tooltip docs and diagnostics, code actions, etc – all out of the box)
- It has an incredible wealth of documented configuration options and a great wiki
- I use it for both linting and language server integration and they work seamlessly alongside each other
Getting started with Coc
When it comes to setup, the Coc README is a great place to start. In this section I’ll include the configuration you need to get it running as well as mappings for things I use all the time, but I encourage you to read through the Coc documentation as well. It is quite comprehensive!
If you use a plugin manager, installing Coc is simple. It won’t do anything unless you have a language server configured, so let’s install the neoclide/coc-tsserver plugin as well:
Plug 'neoclide/coc.nvim', {'branch': 'release'}
let g:coc_global_extensions = [
\ 'coc-tsserver'
\ ]
Coc plugins that we add to g:cocglobalextensions will be automatically installed and updated by Coc.
Note: Many languages don’t have Coc packages, usually because they don’t have custom Coc behavior or configuration. If your favorite language does not have one, it is painless to configure it. For example: configuring the elm language server.
At this point, you won’t have any mappings, but you should start seeing language server errors highlighted with associated icons in the gutter, and cursoring over the errors will show the error or warning message.
In vim8 or Neovim >= v0.4.0 these will display in a floating window. This is what mine looks like:
Additionally, typing should offer auto suggestions along with documentation previews. It should look something like this:
Selecting a completion option from this menu will auto-complete the text at the cursor, and additionally will import the symbol if it is not already imported.
To recap, out of the box we have gotten:
- Language server feedback
- Intelligent language server auto suggestions with documentation previews
- Auto importing
That’s pretty great!
In addition to your vim config, Coc has a configuration file which can be opened for editing using the :CocConfig
command. I’ll refer to this later as the “Coc configuration file”.
In the following sections, we’ll explore some of those options as we round out the experience with a few more nice-to-haves.
Prettier and ESLint
Most projects use ESLint, Prettier, or both. To get started with those using Coc, we just need to install neoclide/coc-eslint and neoclide/coc-prettier. I like to do it conditionally based on whether or not those tools are installed in the local node_modules
folder:
if isdirectory('./node_modules') && isdirectory('./node_modules/prettier')
let g:coc_global_extensions += ['coc-prettier']
endif
if isdirectory('./node_modules') && isdirectory('./node_modules/eslint')
let g:coc_global_extensions += ['coc-eslint']
endif
Errors and warnings from these tools will now show in the same way as from the TypeScript language server.
For ESLint, you must configure which file types you want to lint. You can also enable auto-fix on save. Both can be done via the Coc configuration file:
{
"eslint.autoFixOnSave": true,
"eslint.filetypes": ["javascript", "javascriptreact", "typescript", "typescriptreact"]
}
Note: the eslint.fileTypes
configuration option uses VSCode language identifiers, which is not the same as the output from :set filetype?
._
For Prettier, I found it particularly useful to disable the display of a success message whenever the file is formatted via the Coc configuration file:
{
"prettier.disableSuccessMessage": true
}
Auto formatting
If you like auto formatting, you can enable it for Coc (which enables it for coc-prettier
) and for coc-tsserver
which enables it for the TypeScript language server. These settings are all in the Coc configuration file:
{
"coc.preferences.formatOnSaveFiletypes": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"tsserver.formatOnType": true,
"coc.preferences.formatOnType": true
}
Tool tip documentation and diagnostics
Coc will display diagnostics (errors and warnings) in a tooltip for words you cursor over. We can create a mapping to show documentation for the word under the cursor in the same way:
nnoremap <silent> K :call CocAction('doHover')<CR>
I prefer a more automatic behavior where when cursoring over a word, I see either the diagnostic if it exists, otherwise the documentation. I wrote a snippet to accomplish this:
function! ShowDocIfNoDiagnostic(timer_id)
if (coc#float#has_float() == 0 && CocHasProvider('hover') == 1)
silent call CocActionAsync('doHover')
endif
endfunction
function! s:show_hover_doc()
call timer_start(500, 'ShowDocIfNoDiagnostic')
endfunction
autocmd CursorHoldI * :call <SID>show_hover_doc()
autocmd CursorHold * :call <SID>show_hover_doc()
With this behavior, you can still use the mapping from above to manually show the documentation popup instead of the diagnostic.
Bindings for common actions
I’m only going to scratch the surface here with some examples of the most common code actions. Coc has a great example vim config and documentation that cover all the available actions you can create mappings for.
Navigating
Coc provides several different goto actions. I find the ones for definition, type definition, and references most useful in TypeScript:
nmap <silent> gd <Plug>(coc-definition)
nmap <silent> gy <Plug>(coc-type-definition)
nmap <silent> gr <Plug>(coc-references)
Oftentimes I want to navigate my current file by jumping to the next or previous error:
nmap <silent> [g <Plug>(coc-diagnostic-prev)
nmap <silent> ]g <Plug>(coc-diagnostic-next)
Coc also provides some lists out of the box. neoclide/coc-lists provides even more. Many of these lists are fuzzy-searchable.
You can use the :CocList
command to see the available lists. The one I use the most often is the diagnostics list, which lists the errors and warnings for the entire workspace:
nnoremap <silent> <space>d :<C-u>CocList diagnostics<cr>
A close second is the symbols list, which displays a fuzzy-searchable list of workspace symbols:
nnoremap <silent> <space>s :<C-u>CocList -I symbols<cr>
Performing code actions
Code actions are automated changes or fixes for an issue, such as automatically importing a missing symbol. Code actions can be performed on the word under the cursor with a mapping like the following:
nmap <leader>do <Plug>(coc-codeaction)
In the case of a missing import, I could use the above mapping to prompt me for how I would like Coc to fix the issue:
Renaming a symbol
Coc offers intelligent symbol renaming, a common action in any IDE:
nmap <leader>rn <Plug>(coc-rename)
Final thoughts
Over a period of months, I introduced these various features of Coc into my workflow. It would be a daunting task to learn them all at once!
The depth of configuration available with Coc makes it possible to bring in language server enhancements into your existing vim setup where you want them.