Vim is more useful in the age of LLMs

Published: 08/18/2024

Subscribe Premium $10/month

Checkout with Stripe

Intuitively one might think that vim is less useful in the age of LLMs. The logical chain, which I used to believe, is this:

1. vim is mostly about editing text

2. LLMs will write the text for you

3. Therefore, vim is less useful

But the actual insertion step, after you've positioned your cursor, entered insert mode if necessary and type the code is essentially the same in vim, vscode or any other editor. To the extent chatgpt or copilot or whatever is writing the code for you, you actually spend more time yanking text around, exploring the codebase, and debugging than you do typing. And that is where vim could plausibly make you more productive. Not in the actual typing of code.

Perhaps one day you'll mutate your codebase without ever touching an editor just giving a high level request like "Add a button with this functionality" and the LLM will figure out where in the code base to add the button, the front and backend functionality, the tests, and any other changes your change requires. [1] . But while we're in a hybrid regime where it's still necessary to write and understand the code you just have a smart but low context and creativity assistant, vim is actually more useful.

Another perk for vim in the age of LLMs is that it's never been easier to learn vim. LLMs are familiar with the history and all the commands and motions. And if you want a more complicated behavior your llm will be happy to chain the functions together for you. For instance last week I was curious how many lines [2] I had open across buffers so I asked Claude-3.5 Sonnet and it gave me the following command:

:echo reduce(filter(map(range(1, bufnr('$')), 'buflisted(v:val) && linebufnr(v:val) > 0 ? linebufnr(v:val) : 0'), 'v:val'), {acc, l > acc + l}, 0)

I asked how to get the same functionality in VSCode and it gave me an 8 step process that started with "Install Node.js" and ended with "Load your extension". Good luck!

This is sort of a toy example. But for toy examples a gain in efficiency is the difference between satisfying your curiosity or not doing it. Two other more valuable recent scripts that I got from my LLM and you may find useful are copying github links and copying markdown blocks. I use them both most days now and simply wouldn't have spent the ~30 minutes necessary to write them unassisted.

The following command copies a link to the line of code on github. Claude one shotted this except for the branch name which it hard coded to main before I asked it to use git and the mapping to run the function which I added myself.

" Github
function! CopyGithubLink() abort
  " Save the current working directory
  let l:original_cwd = getcwd()
  " Change to the directory of the current file
  let l:buffer_dir = expand('%:p:h')
  execute 'cd' fnameescape(l:buffer_dir)
  " Get the current file path relative to the repo's root
  let l:file_path = system('git ls-files --full-name ' . shellescape(expand('%')))
  if v:shell_error
    echo "Not a git repository or file not tracked"
    execute 'cd' fnameescape(l:original_cwd)
    return
  endif
  let l:file_path = trim(l:file_path)
  " Get the line number
  let l:line_number = line(".")
  " Get the origin URL from git
  let l:origin_url = system("git config --get remote.origin.url")
  if v:shell_error
    echo "Not a git repository or no origin remote"
    execute 'cd' fnameescape(l:original_cwd)
    return
  endif
  " Get the current branch name
  let l:branch_name = system("git rev-parse --abbrev-ref HEAD")
  if v:shell_error
    echo "Could not determine the current branch"
    execute 'cd' fnameescape(l:original_cwd)
    return
  endif
  let l:branch_name = trim(l:branch_name)
  " Sanitizing the origin URL
  " Trim newline and other trailing whitespace
  let l:origin_url = trim(l:origin_url)
  " Convert SSH URL to HTTPS URL if needed
  if l:origin_url =~? '^git@'
    let l:origin_url = substitute(l:origin_url, '^git@\(.*\):\(.*\)$', 'https://\1/\2', '')
  endif
  " Remove ".git" suffix if present
  let l:origin_url = substitute(l:origin_url, '.git$', '', '')
  " Construct the URL to the specific line in the file
  let l:github_link = l:origin_url . '/blob/' . l:branch_name . '/' . l:file_path . '#L' . l:line_number
  " Copy the GitHub link to the system clipboard
  let @+ = l:github_link
  echo "Copied to clipboard: " . l:github_link
  " Restore the original working directory
  execute 'cd' fnameescape(l:original_cwd)
endfunction
" Create a command to call the function
command! CopyGithubLinkToClipboard call CopyGithubLink()
nnoremap <leader>gh :CopyGithubLinkToClipboard<CR>

Copy markdown block

The following copies code in markdown with yi` while preserving the default vim behavior which doesn't work across lines. It's extremely useful when you're talking to an LLM in vim [3] because they tend to use markdown code blocks to box their code snippets. The LLM almost one shotted it but it had an off by one error, I had to tell it to not count triple apostrophes that don't start the line and it was a lot of work to get it to treat registers correctly so for instance "ayi` would copy the text to the a register. It kept hardcoding the register. Maybe I wasn't asking clearly. Or maybe people usually don't make their personal mappings work with registers so there's not that much training data.

" Function to yank markdown code block or inline code
function! YankInlineOrBlock(register) abort
    " Save current cursor position
    let l:save_pos = getpos('.')
    " Decide behavior based on current line's content
    if getline('.') =~ '\m^\s*```'
        echo "Invalid position for yi` command"
        return
    endif
    " Check for the presence of backticks on current line
    if getline('.') =~ '\m`'
        " Yank inside inline backticks and respect register
        execute "normal! " . a:register . "vi`y"
    else
        " Yank inside triple backtick block
        " Move cursor to the nearest opening triple backtick above
        let l:line = search('^\s*```', 'bnW')
        " Check if we're in a proper markdown block
        if getline(l:line) !~ '^\s*```'
            echo "Not inside a markdown code block"
            call setpos('.', l:save_pos)
            return
        endif
        " Move cursor to the start of the block content
        call cursor(l:line + 1, 1)
        " Yank until the closing triple backtick
        let l:end_line = search('^\s*```', 'nW')
        " Check if we found the closing triple backtick
        if getline(l:end_line) !~ '^\s*```'
            echo "Unmatched markdown code block"
            call setpos('.', l:save_pos)
            return
        endif
        " Put the block content into a register (default register " and for operations)
        let lines = getline(l:line + 1, l:end_line - 1)
        call setreg(a:register, lines, 'l')
        " Restore cursor position
        call setpos('.', l:save_pos)
    endif
endfunction
" Remap yi` to call the function, allowing for explicit register use
nmap  yi` :call YankInlineOrBlock(v:register)

Wider Significance

This and anthropic artifacts make me think the future of code could involve a lot of bespoke software made just for one individual's use case. Making software has historically been so expensive that for all but a few use cases one should take the off the shelf solution even if this involves significant compromises. But this balance is shifting. And software that lends itself to customization and plugins is therefore more valuable.

[1] That's what we're working on with mentat but we're still a ways away and I'm still not certain such as thing will actually be possible in the short term. Seems inevitable in the log run

[2] 895791 lines were open if you're curious. Big "that's it" moment.

[3] Which I recommend doing. I use this plugin I wrote to talk to my LLMs in vim. It's pretty simple so you should plausibly just write your own. Or rather ask Claude to write it for you