(Neo)vim tips and tricks: Volume One
Intro
I flip back and forth between editors. Usually I'm in Sublime but I'm back using NeoVim for the last few weeks. Subsequently, I've picked up a few things using it, and being back in the Terminal full time, that I thought would be worth sharing.
Copying/Moving lines to marks
There's a common scenario where you need to copy a few lines from one place to another. You may already know that with Vim you can do :152,154t.
and that will copy lines 153 to 154 to the current line. If I wanted to move them instead of copy them, :152,154m.
would do that.
If you use relative line numbers, that works fine too.
That works well when the lines you want to copy are visible on screen. What if they are 'up there' somewhere (motions up the file)? Keeping the source and destination line numbers in your head is an unneeded burden. This is where marks come in.
Suppose I am way down on line 453. I know there is a line(s) somewhere up top that I want to grab, and insert here on line 453. I can mark the line with ma
(Mark, register 'a'), then skip up to the line I want (search backwards or whatever suits). Once I am at the line(s) I want, I can use :.t'a
to copy the current line to mark 'a'. Or :.m'a
to move it there. Vim will also take me down to the mark as part of that command.
If you don't even want to think about line numbers, you could do a similar operation with a visual selection. Make your mark, move to the area you want to move/copy, press :
and you will get the range indicator of your current selection (:'<,'>
) and then add t'a
or m'a
, so the whole thing looks like :'<,'>t'a
.
Toggle netrw in and out
I remember this used to be a giant pain in the ass. If you had a couple of splits, opened netrw and then did :bd
to close it, it would take the split with you. You could also do ctrl-^ to go back to the buffer underneath and then some kind of :Rex
to get back, which according to my anecdotal data worked about 73% of the time.
Imagine my joy when I found out that Neovim has :Lexplore
to just toggle that sucker in and out. I have that mapped to leader e
and it works generally great. The only bugbear I have still is that it doesn't put me back in the same split. Something I need to fix!
Setting an environment variable for your init.lua
I had a situation where my office computer, sat behind a corporate firewall, had LSP servers installed into an odd location (for reasons I can't explain). The upshot was that LSP wasn't running on my standard config. I just got a cmd ["vscode-css-language-server"] is not executable.
message from a file that LSP should have worked with.
Thankfully, we can add a little control flow into the init.lua
to deal with such a problem, providing one path when in one environment, and another when it another.
Now, you might need to get at environment variables for totally different reasons, but the logic will be the same.
I'm using zsh as my shell, so the first thing needed is to set an environment variable there. In the .zshrc I added this line:
export MACHINE=work
And if you open a new shell you can check that is working by running echo $MACHINE
.
Then in my init.lua
I added this:
-- different path for CSS lang server on office machine
local envMachine = os.getenv("MACHINE")
if envMachine == "work" then
machineCmd = 'SPECIAL PATH HERE'
else
machineCmd = 'vscode-css-language-server'
end
And then in the init.lua
, used that local variable machineCmd
as the cmd: cmd = {machineCmd, '--stdio'},
for the LSP setup.
Getting a lua formatter installed for easier init.lua work
I use a init.lua for my NeoVim config but as I'm new to Lua I'm not always 100% on the syntax of things. One thing that really helps is a code formatter because when I get stuff wrong it breaks and tells me I'm an idiot so my feedback loop is much shorter. Plus it also nicely formats the code as I go.
The most up to date lua formatter is Stylua and for the macOS users it's now on Homebrew too. brew install stylua
. Then its just a case of wiring it up with your code formatting plugin of choice. I'm using Formatter here.
Then you want to configure Formatter in your init.lua like this:
lua = {
-- stylua
function()
return {
exe = "stylua",
args = { "--indent-width", 2, "--indent-type", "Spaces" },
stdin = false,
}
end,
},
And then that's basically it. Write Lua, break things, fix things, rinse and repeat. I'm obviously aware that the idea of 'rinse and repeat' is a little lost on me (brushes hand over skin on top of head).
No homebrew? No problem
If you can't use Homebrew in macOS you need to get your hands a little dirty.
You can download the StyLua binary, get it copied into our $PATH and then we can configure Formatter to make use of it.
- Download the binary (https://github.com/JohnnyMorganz/StyLua)
- Copy it to a folder on your path (e.g. /usr/local/bin/) and then from
/usr/local/bin/
runchmod +x stylua
to make it executable. You should then be able to verify it works withwhich stylua
If you get a security error from macOS when you first run it, you need to open the stylua file from Finder first and confirm it is OK.
Center the filename in Lualine, add a basic word count
I'm using Lualine here and one of the changes I wanted to make was having the filename I'm working on appear in the center. I managed to lift this little snippet from the projects GitHub issues:
lualine_c = {
{"diagnostics", sources = {"nvim_lsp"}},
function()
return "%="
end,
"filename"
},
That little function in there that returns %=
lets me have the LSP diagnostics in the C section alongside the filename but positions the filename in the center. There's a link to my init.lua below so you can lift it from there if that appeals.
Also, I like having a word count, especially for text and markdown. So, you can add a count like that into Lualine, with a little local function:
local function getWords()
return tostring(vim.fn.wordcount().words)
end
And then insert it in whichever section you like:
{ getWords },
That works but it's as basic as you can imagine. I'll look to expand the functionality so it only shows for certain files, and also works with visual mode; only showing me a count for a selection.
Check which plugin is using a shortcut
I was trying out the Lightspeed plugin, which is similar to Hop, Sneak and EasyMotion and thought there was a bug. I should have a known it was an error between keyboard and chair!
You can actually check what is using which key by using echo maparg("S", "v")
where the first argument is the key you are interested in and the second argument it the mode.
Get nested code blocks syntax highlighted in markdown
I picked this tip up from the VimTricks website. I had no idea that Vim has this built in and it's just a case of enabling it.
Every piece of writing I do tends to start as a markdown file. As my books and blog posts contain lots of code samples, it's particularly useful to see syntax highlighted code blocks. And you just need to enable this in Vim!
This is how I'm adding this in my init.lua:
-- Give me some fenced codeblock goodness
g.markdown_fenced_languages = { "html", "javascript", "typescript", "css", "scss", "lua", "vim" }
And that means whenever I'm writing documentation or blog posts I can stick in a code block and see it nicely rendered as I go. You just have to remember the correct tag next to your opening backticks.
Jumping to the line number of a file with Telescope
A great feature in Sublime Text (by the way, you should totally check out my course on Sublime Text) is that the fuzzy file picker lets you add a colon so you can jump right to a particular file and number. This is handy when you have a console error or similar.
I wondered if the functionality could be added to the Telescope plugin.
I asked a question on the Telescope gitter chat:
Could Telescope be amended so it is possible to open a file at a line with the file finder? For example with a colon? So once I search and my file is selected I add :34 to go to line 34?
And within 40 minutes, a chap called Fabian David Schmidt / @fdschmidt93 had supplied some cut and paste goodness: https://gitter.im/nvim-telescope/community?at=6113b874025d436054c468e6
So I have this in my Telescope config now:
find_files = {
on_input_filter_cb = function(prompt)
local find_colon = string.find(prompt, ":")
if find_colon then
local ret = string.sub(prompt, 1, find_colon - 1)
vim.schedule(function()
local prompt_bufnr = vim.api.nvim_get_current_buf()
local picker = action_state.get_current_picker(prompt_bufnr)
local lnum = tonumber(prompt:sub(find_colon + 1))
if type(lnum) == "number" then
local win = picker.previewer.state.winid
local bufnr = picker.previewer.state.bufnr
local line_count = vim.api.nvim_buf_line_count(bufnr)
vim.api.nvim_win_set_cursor(win, { math.max(1, math.min(lnum, line_count)), 0 })
end
end)
return { prompt = ret }
end
end,
attach_mappings = function()
actions.select_default:enhance {
post = function()
-- if we found something, go to line
local prompt = action_state.get_current_line()
local find_colon = string.find(prompt, ":")
if find_colon then
local lnum = tonumber(prompt:sub(find_colon + 1))
vim.api.nvim_win_set_cursor(0, { lnum, 0 })
end
end,
}
return true
end,
},
And now when I search for files I can add a colon and then pass the line number and open the file at that point. Super handy when you want to jump to the source of an error or, more often than not, a console log I forgot to delete. Amazing.
Emoji picker with Kitty
I'm stretching things to say this is a Vim tip as it is actually to do with Kitty, which is the Terminal I use currently. Imagine you're in insert mode and you realise you need an Emoji. In My case, I wanted a bullet point. Now in macOS I can press ctrl+cmd+space
to bring up the Emoji picker at any time. Well with Kitty, I can use the same shortcut.
So, in Insert mode, press that same ctrl+cmd+space
shortcut and you get this emoji picker. I just have to press the full stop (or period if thats your parlance), and then press the key next to the emoji I am after.
Now this isn't searchable, it's only showing your recent Emoji's but on most occasions it is all you need.
Increment search results based on the iteration number
Sublime Text has fairly approachable arithmetic capabilities. It makes incrementing numbers in selections pretty trivial and I wondered how the same problems were solved in Vim.
So the kind of use case I'm talking about is, suppose you have a file and littered throughout are a bunch of placeholder strings for images, they all have 1.gif
at present.
I was looking for a way to search for that string and increment the number each time, so the first result becomes 2.gif
, the next 3.gif
etc
I was thinking there might be some way of searching for a string, then using gv
to select the last result and then increment the number in the selection with <C-A>
plus the iteration number. However I couldn't find anything that exposed the iteration count of the search result.
Anyway, I asked on Stack Exchange and was provided a solution within minutes. It looks like this:
:let idx=0 | g/2.gif/ let idx += 1 | s//\= idx . '.gif'/
And here is the result.
It works by setting a counter, then using a global command it increments the counter and does a search and replace on the existing number with the value of the counter.
Now I'm not sure I'll ever remember that next time I need to use it but it's nice to know there is a way to do this sort of thing without needing to look for a plugin.
Line jumps work with arrows too
I didn't realise that number based jumps work with numbers too. So, all the tutorials will tell you that you can do 5j
to jump down 5 lines, or 5k
to jump up 5 lines. Well on my Colemak layout, that's a litte confusing as the two keys are the opposite way around, unless you imagine you are using a flight yoke or something. Anyway, those same line movements work just as well for jumping lines with the arrow keys. So you can do 5↑
or 5↓
. That won't help you when it comes to writing commands or macros but it helps me day to day.
Add a line, in multiple files at a line position. Without opening those files!
Maybe you have had a similar sceario. I want to add the string "status": "not started",
to multiple JSON files. Now the string and the file type are unimportant. The point is, we want to add a line to a specific line position in multiple files. The more files you need to do it on, the more worthwhile this approach will be.
To start this, you need the files you want in the QuickFix list. I tend to use Telescopse to get files into the Quickfix list, but you can do it anyway that works best for you.
There are a couple of ways we can do this. Both using the command line mode. Both of these solutions use the 'CuickFix Do' command.
:cfdo execute 'norm 5GO"status": "not started",' | update
This one uses execute
so we can execute wnat's in the string that follows. So, in this case I'm using norm
al mode, then using 5G
to jump to line 5, then O
to add a line above, and then the string I want to insert. Note the use of different quotes so I don't need to escape the double quotes. Then we use the | update
to save the files. You can omit that part if you don't want them saved.
When I asked about how to do this on 'Vi and Vim Stack Exchange' I also got a completely different approach suggested:
:cfdo :call append(4, '"status": "not started"')
This uses :call
to use the append
command, which I didn't know about at this point, to append the text. This feels a little cleaner to me as it's semantically exactly what I want to be doing, without needing to think about how you would move in a buffer to achieve the same action. Very nice.
Operate on multiple, non consecutive lines
Imagine you are working on a file that has a bunch of console
commands that you want to toggle off. They are littered throughout the file.
We can use Vim's Global g
command to comment them all in one go.
:g/console.log/ normal gcc
In this case we use the g
so search for the string we are interested in, then switch to normal mode to operate on those selections. In this case I use Kommentary plugin to comment and uncommment lines and that shortcut comments them.
Embrace suspending Vim
I remember when I had my first stint with Vim, accidentally pressing ctrl+z
instead of ctrl+a
and pancicking as my Vim vapourised. After that I remapped that key so it wouldn't do that. Nowadays, I've come to understand this as a great feature of the Terminal. I use it all the time to drop back to the main Terminal, and commit some code before using fg
or "foreground" to go back to process I was in, neovim in this case.
If the same thing has happened to you, I'd encourage you to take the time to embrace ctrl+z
– your stuff is exactly how you left it, and you just need to head back to the foreground to get it back.
Incidentally, if you have got into a real mess, you can type jobs
in the shell to see what is going on. See the number there? If there is more than one thing, you can foreground the one you want with %[number]
. If there is just the one, you can also press just %
which is a way of saving a keystroke over fg
!
Conclusions
If this was useful, let me know and as I gather more of these things as I go! Everyday is a school day with Vim and I enjoy passing these things on if they are useful.
Hi! Here is an elegant solution for those moments where you end up sending a running process to the background.
“`sh
# this works on zsh
fancy-ctrl-z () {
if [[ $#BUFFER -eq 0 ]]; then
BUFFER=”fg”
zle accept-line
else
zle push-input
zle clear-screen
fi
}
zle -N fancy-ctrl-z
bindkey ‘^Z’ fancy-ctrl-z
“`