Modern nvi mappings

Updated on .

Once I was happy with my customizations to Sublime Text, I needed a new way to procrastinate, so I started trying to add “modern” features to The nvi text editor without modifying its code. After Configuring the defaults of nvi, I wanted to see how I could further extend the editor in ways its creators didn’t foresee. I eventually was able to add features like a fast file browser, Git blame in a split window, and multi-file search. These customizations rely on knowing how Entering control key chords into nvi works.

Back to the shell

In a terminal-based editor, it’s nice to be able to quickly get back to a shell prompt. To do this, I have the following mapping for typing space bar twice:

map ^V ^V  :w^M:suspend^M

That’s control-v three times, then space bar, then control-v three times, then space bar twice, followed by :w and control-v, then control-m, etc. The :suspend command works like the control-z job control command in other programs.

Paragraph wrapping

map Q {j!}par P+. Q+__* g1 w80^M

This command wraps the paragraph the cursor is in to 80 columns. Unfortunately, it doesn’t handle C /* */ comments well.

macOS clipboard

nvi lacks system clipboard integration so its own copy and paste buffers are separate from the clipboard used by other applications. There’s no mouse support, and the mouse cannot select more text than fits in the window, so Terminal.app’s copy command won’t always work (or at all, if line numbers are visible). Pasting text is also difficult since nvi will try to auto-indent based on the structure of code, and doesn’t pay attention to paste delimiting control characters. To workaround these issues, the macOS pbcopy and pbpaste commands need to be available through key mappings that intentionally move text between nvi and the system clipboard. Even when using vim, I preferred using dedicated mappings for this, to prevent temporary text movement (any deleted text goes to vim and nvi’s paste buffers) from clobbering the system clipboard. Here are the lines of my ~/.nexrc for adding these key mappings:

map ^V y :^Rp:.,$!pbcopy^M:q^M
map ^V p :read !pbpaste

Typing space followed by y takes whatever nvi has most recently yanked and puts it into the system clipboard. And space followed by p inserts the system clipboard contents into the file.

The pbcopy mapping takes advantage of the split window opened for command editing as a scratch area for “filtering” the contents of the paste buffer through the pbcopy command. :^Rp opens the command editor and pastes into it, :.,$ addresses the current cursor position to the end of the command editor, and !pbcopy^M passes that text to the pbcopy command as input, replacing it with its (always empty) output.

The pbpaste mapping makes use of the ex read command, which was purpose-built to read the output of commands like date or ls into nvi.

This (and future maps) rely on using control-r for the command editing cedit setting:

set cedit=^R

Now, typing control-r while in ex command mode (:) will open a horizontal split window to edit previous commands and create new ones. While it’s possible to use the current file for these key mappings, nvi will register that the file has been modified and warn before closing. Since I often open a file and copy some text from it without modifying it, this happened most of the time I used this mapping. The command editor is the only buffer that nvi opens which doesn’t have this feature.

Manual pages

When working in systems software, getting to a manual (man) page quickly is important. I’ve bound K to open the man page of the word under the cursor:

map K wb"zyw:^Ro:!man ^["zp^M

The wb moves to the beginning of whichever word the cursor is inside of and "zye yanks that word into a buffer labeled z, to be used later without clobbering the default yank buffer. :^Ro starts editing a new command in the command editor in insert mode and then :!man is written as the first part of the command. ^[ acts like the escape key and returns to normal mode, where "zp puts the contents of the z buffer onto the line. ^M is a carriage return, which executes the command: !man <word-under-cursor>.

This causes the manual page to show up, and when it’s closed, control is returned back to the editor. If you’d prefer the man page to open in a separate window, use the open command and Terminal.app’s x-man-page:// URL scheme:

map K wb"zyw:^Ro:!open x-man-page://^["zp^M^M

The extra ^M is necessary to acknowledge that the command has finished.

Opening files

While nvi does have tab-completion for opening new files, I’m a lot more used to navigating to new files using a fuzzy search for their file names. The fzf tool works well for me, but any utility that can run a command to generate the list to search in and then prints the selected line to stdout will work. I’ve bound the command to space, then f:

map ^V f :^R:!env FZF_DEFAULT_COMMAND='git ls-files' fzf >> %^M:edit^MGIEdit ^[^M^W:bg^M

This mapping uses the command editor, like before, but redirects the output of a command to the temporary file that nvi backs it with using >> %. Then, :edit^M reads the contents of the file back from the file system, goes to the last line of the file with G, and prepends a command to edit the path in the last line with IEdit before going back to normal mode with ^[. Edit differs from edit in that it opens the new file in a split window, with two buffers open at once. ^W moves the cursor back to the existing window and :bg puts that window in the background, keeping it open (and keeping any unsaved modifications) but filling the screen with the new file.

I usually start a search for a symbol or word I want to learn more about by looking for all occurrences of it in a project (as long as it’s relatively unique). If the project is tracked by Git, the git grep command is very fast. I bind the ampersand character to search for the word under the cursor:

map & wb"zye:^Ro:!git grep ^\ ["zp^M

This follows the same formula as the mapping for showing a manual page and temporarily displays any lines that contain this word across the Git repository. But for this listing, it would be nice to be able to open one of the lines in nvi. To do that, I added a fuzzy search that will open the appropriate line of that file, like the fuzzy file name search mapping:

map & wb"zye:^R:!echo % >> %^M:edit^MGI:!git grep  ^[h"zpwi^X7c fzf >> ^[^M:^R:edit^MGf:l"zyt:hDIEdit + ^[h"zp^M^W:bg^M

For the most part, this mapping is an extension of the file opening fuzzy finder, with a custom data source. The !echo % >> % is needed to get the command editor’s file path, in the context of the command editor (it won’t work to have fzf redirect to %, since that refers to the file being edited). The ^X7c enters the pipe character using its ASCII hex code, to prevent it from being interpreted in the ~/.nexrc file as ending the mapping. And the Edit command takes an optional first argument, +<line-number> to jump immediately to that line when the file is opened.

Side-by-side git blame

map gb :vsplit^M"zy1G:edit /tmp^MG:read !git blame -- #^M:^R^Mo^["zp!Gwc -l^[I:^[^M

This is a continuation of the ideas from previous macros, so I’ll leave its analysis as an exercise for the reader.