Skip to main content
  1. Posts/

Neovim Workflow

·2196 words·11 mins
Author
Yang Hu

Neovim comes with lots of useful plugins, and built-in features, compared to Vim. It’s lua configs is also much more readable and powerful compared to Vim. Here’s some useful tips for coding in Nvim.

Searching(files, text, diagnostics, help)
#

Similar to techniques mentioned in search-in-vim, we can search for text inside files, or looking for files by filtering filenames. In Neovim, we use fzf-lua which is very similar to fzf.vim.

Using FZF picker

FZF’s UI consists with a picker (fuzzy search a list of entries) and a preview window. In picker, use c-j/k/n/p(or c-u/d) to move. Tab/S-Tab to select files. The default action (Enter) is to edit or send to qflist, depending on number of selections. If want to edit multiple files, use c-e.

Another useful command is “resume”(<Leader>sr), which picks up the previous FZF search.

Find files with fuzzy matching
#

If you know which file you want to open, using the files picker and fuzzy search. I created different keymaps for looking for files in different scopes.

UseKeymapDetails
Files in PWD<Leader>fPWD
Recent files (MRU)<Leader>oVery useful to search through previously opened files
Sibling files of buffer<Leader>s.Also includes files in subfolders
Project-wide files (git)<Leader>gfOnly sees git files
Any path starting with buffer dir<Leader>sfCan modify path before press Enter

Search text
#

If you want to find some text among files, grep is the way to go. fzf allow you to grep then fuzzy search the results for refinement. Similar to find files, there are different keymaps for grepping in different scopes.

Grep in files
#

UseKeymapDetails
Files in PWD<leader>/Use keyword -- glob to filter files. (! for nagative patterns)
Git root<leader>g/Same as above
current Word/WORD<leader>w/Ww also works in visual mode

live grep: Both keymaps uses “live grep”, meaning each keystroke would run a new ripgrep command and feed the result. this means we can test our grep expressions on-the-fly.

glob: In live grep, globs can be added with -- after keywords. Example: -- *.lua !*.md lib/**/*.c.

  • * means any character
  • ** means any folders (recursively)
  • ! means negative matching.

So, our example expression *.lua !*.md lib/**/*.c means, grep on lua files, and all c files under lib/, but excluding any markdown files.

Fuzzy search buffer lines
#

Besides real grep, two convenient methods allows fuzzy-searching lines in buffers:

UseKeymapDetails
Search current buffer lines<leader>sb[S]earch [B]uffer lines
Search all open buffers<leader>so[S]earch [O]pen buffer lines

Grep git repo stuff
#

Git repository contains other useful info that we usually want to search for, like (current buffer) commits, branches, etc. FZF provides a convenient way to search through them.

The most useful one would be search commit history of current buffer, via the :FzfLua bcommits command. I’ve created two keymaps for them:

UseKeymapDetails
Search commits containing current buffer<leader>gb[G]it [B]uffer commits
Search all commits<leader>gc[G]it [C]ommits

Search vim help
#

FZF also is convenient to search for help tags and keymaps. (through sh and sk followed by <leader>). There’s also one for Man pages with <leader>sm.

Treesitter
#

Treesitter parses files into code objects, which can not only provide better highlighting, but also brings a whole new set of motion objects, that can use in both operation-pending mode and visual mode. This allows a much easier and more accurate text manipulation.

Treesitter objects for selection and motion
#

I’ve defined text objects with the following letters:

  • c: Classes
  • m: Methods/functions definitions
  • f: Function calls
  • i: Conditional (if) statements
  • l: Loops
  • a: Args
  • =: assignments

All objects support jumps to prev/next via [ and ]. Upper case goes to the end while lower case goes to the beginning. Here’s a few examples:

  • [c: start of prev class
  • ]L: end of net loop
  • [f: start of prev function call

A more powerful way to use TS objects is via selection like other text objects like words, paragraphs, etc. Visual selection is one way to do it, but it’s more powerful with operation-pending mode. Some examples:

  • cr=: [C]hange [R]hs of [=]
  • caa: [C]hange a [A]rgument
  • cii: [C]hange [i]n [I]f: This is very useful, depending on cursor position. It selects the “if” condition, or selects the conditional body when cursor is inside the body.
  • daf: Delete a function call.

I also have Flash plugin to quick select a block. Triggered by S or R.

  • S: select a block from current cursor position
  • R: select a remote block. Type the letters in the remote block, then colored labels will appear to allow you to select blocks.

[x is another special and useful hotkey. If the cursor is deeply nested several levels down, the current scope will show at top of the screen. [x jumps to the start of a “upper” scope.

See it in action:

Outlines
#

Treesitter also provides more accurate outlines of documents. I installed aerial.nvim for showing outlines.

  • To toggle aerial window on the side: <leader>a (<leader>A stays in aerial window)
  • To jump to next/prev outline item: <leader>{ and <leader>}

LSP and diagnostics
#

Neovim supports LSP natively. It brings diagnostics, signature help, documentation, code actions and formatting capabilities.

Diagnostics
#

Diagnostics shows info, warning or errors of the code. My default setting will show diagnostics icons on the gutter, and virtual text in each line.

Work with diagnostics
#

UseKeymapDetails
Previous diagnostics[d
Next diagnostics]d
Toggle diagnostics<leader>dtToggles virtual text
Show floating box<leader>dfShows float box with details
Fzf search diagnostics<leader>sdSearches document diagnostics

Once at a diagnostics point, you may see code actions at the cursor position, which may provide useful fixes to the issues.

Show code actions<leader>caSome LSP may not provide useful ones

You can also put diagnostics to quickfix list so you can process with commands like :cdo / :cfdo or :ldo / :lfdo

Send to quickfix list<leader>dqCan then use :cdo or :cfdo to process
Send to location list<leader>dl

Accessing diagnostics with trouble.nvim
#

Buffer Diagnostics<leader>xXToggle the trouble window
Workspace Diagnostics<leader>xx
Quickfix list<leader>xq

Rename and code actions
#

Rename<leader>rnRename via LSP API
Code action<leader>ca

Auto complete
#

keymaps
#

UseKeymapDescription
Prev/Next<C-j> / <C-k>
Close<C-c>
Scroll<C-b> / <C-f>
Confirm<C-y>Not CR or TAB
Move next/prev location<C-h> / <C-l>Work in INSERT mode, Useful in snippets
Next choice<C-e>For choice node in snippets

Snippets
#

Luasnip is the plugin to provide snippets to auto completion.

My own snippets are created and located at dotfiles/nvim/lua/snippets/, filenames needs to match the filetype.

I also created helper snippets for lua so that creating new snippets skeleton is super fast. The snippets can be triggered with:

  • snipf: For complex snippets with i nodes, etc.
  • snipt: For simple text snippets (no expansion locations or dynamic stuff)

To create snippets, create a [ft].lua file, then use the snippets above to quickly add new content.

My Snippets
#

Markdown

  • sc for Hugo shortcodes.

Plugin details
#

Auto complete is provided by nvim-cmp. It take completion choices from configurable sources, like LSP, snippets, path, buffer, etc.

A list of cmp sources can be found at nvim-cmp Wiki

Formatting
#

Auto format
#

I use conform.nvim to format files. Auto cmds are created to format on save for certain file types. I currently use a whitelisted approach instead of let formatters to format files by default, as I don’t want to break stuff, and manually triggering formatting is not that hard with the keymap:

UseKeymapDescription
Manually trigger formatting,fCalls Conform
Format injected code,mfLike code blocks in Markdown

conform.nvim is a “middleman” that calls external formatters (and fallback to LSP), so you need to configure formatters for different filetypes, and install the formatters separately, following their own doc.

See :h conform-formatters

Formatter setup
#

  • Prettierd config to format markdown with 80 column width

By default, prettier(and prettierd) will not break lines for markdown files. I needed to create a config override for prettierd to force it.

  1. Create a custom config file with this single line and save it to .config/nvim/utils/linter-config/.prettierd.toml
1
proseWrap = "always"
  1. Set environment variable for prettierd in conform’s formatters section:
1
2
3
4
5
6
7
8
formatters = {
	-- Always wrap markdown files to text width
	prettierd = {
		env = {
			PRETTIERD_DEFAULT_CONFIG = vim.fn.expand("~/.config/nvim/utils/linter-config/.prettierrc.toml"),
		},
	},
}

Jumps with leap and grapple
#

Leap within the reach of your eyes
#

To quickly jump in the current view, use leap. It’s like using a mouse: jump to where your eye is looking at. (in contrast to regular Vim motions that moves over “text objects” that has some meaning)

How to use

Starts with s, then type two letters. You’ll either jump to there already, or need to press a label key. Since the label is displayed from beginning, typing the labels won’t slow you down.

Another important tip is using gs jump to the other window, which saves the window move.

Some special characters when using leap:

  • <space> end of line,
  • <space><space>: blank lines
  • All types of brackets are considered equivalent

Remote operation
#

It’s quite often that we want to modify a remote location like yank something and put it where the cursor is. Normally, we’ll have to:

  1. Move to the remote location
  2. Perform the operation (yank/delete/etc.)
  3. Jump back with c-o
  4. Paste or continue typing

Steps 1 and 3 can be saved with remote operation. When in operation-pending mode, press “r” to trigger it. Now the new process is

  1. Want to yank something remotely, press y, then r to enter remote mode.
  2. Start typing to match the remote location, and press the label key. Now the cursor is in the remote location
  3. Type the motion like aw, i{, etc. Operation is done on the remote text and now you’re back where you started.

Syntactical selections with flash
#

When writing code, you can easily select a syntactical block, with the help from treesitter and flash.

FeatureKeymapComment
Select a block containing current cursorSShows labels around different levels of blocks
Select a block remotelyRAfter press R, start typing the remote location text, and labels will appear for selecting blocks around that location.

Jump between files
#

When working on some projects, we often jump between a few important files. Examples like {build, test, header, impl, reference}. Switching buffers is still not fast enough: buf list is usually noisy with other stuff. Even with fzf, we need to type words for filtering each time we switch.

Grapple allows marking files with tags, and cycle through them. It’s like global marks, but with names, project scoping, and better UI. Here’s the common workflow. We also have status bar integration to show all tags and active status.

UseKeymapDetails
Add tag<Leader>maCan enter an optional name for the tag.
Remove a tag(untag)<Leader>mdUntag the current file.
Move to next tag<Leader>nI only mapped foward-move
Show all tags and select<Leader>mkProvides single-key hotkeys to jump

File management
#

oil.nvim allows you edit your file system like a vim buffer. On save, it applies the changes to the file system. It much more convenient than doing shell commands to create folders, move files, etc.

oil.nvim is configured to be the default file explorer in nvim and can be triggered by nvim . in shell, or like :e . in cmdline.

Working with Git
#

Two plugins are spciefically for git:

  • gitsigns: Gutter signs for diff, with helpers to stage/unstage and show inline diff, blame, etc.
  • Fugitive: classic plugin.

fzf.lua also provides pickers for search git commit history.

Inline diff and staging
#

When editing files, gitsigns makes it easy to:

  1. Jump through all hunks of changes in the file ([h, ]h)
  2. Toggle inline diff (against staged version, or last commit) (hd, hD, hp)
  3. See blame info (hb)
  4. Stage/unstage hunks (hs, hS, hu)

All keymaps are under <leader>h (hunk)

UseKeymapDetails
Move between changes/hunks[h / ]hHunks are changed blocks compared to staged version.
Preview hunk diff<Leader>hpInline preview of diff of this hunk.
Diff the buffer(side-by-side)<Leader>hd / Leader>hDd: diff against staged version; D: against last commit(~)
Quit diff view<Leader>hq
Stage buffer/hunk<Leader>hS/hsUpper case for buffer, lower for hunk.
Reset buffer/hunk<Leader>hR/hrUpper case for buffer, lower for hunk.
Unstage hunk<Leader>hu
Blame line<Leader>hbshows full blame in float window, with commit, hunk, etc.
Toggle blame inlay<Leader>htbShows blame of curline in normal mode
Toggle deleted hunk display<Leader>htdShow/hide deleted hunks

Stage files and Create commit
#

Staging buffers while editing using gitsigns is convenient. Another way to see the full status of the project and do stage/unstage, see diff and create commits are using Fugitive’s “status” view.

  1. Open git status with <leader>gs
  2. Move between files using (, )
  3. Toggle diff of file under cursor with =
  4. Toggle stage with -. (or use s, u)
  5. hard reset a file with X
  6. Create a commit using cc

Handling Merge Conflicts
#

Explore git history with Fugitive
#

  • Commits / logs
  • History files
  • Diff

Misc
#

Preview markdown files
#