Vip - Vi-Style Editor in PicoLisp

A complete editor in less than 1000 lines of code

There is an old saying: Once in your life you should build a house, plant a tree, and write an editor. I decided to start with the last one.

I implemented a subset of Vim in PicoLisp. It feels and behaves almost identically to what I'm used to, and what I have configured in my .vimrc. It even has some features improved over Vim. I never really understood the quirks of vimscript, and it is easier for me to write the whole beast in PicoLisp.

Not that the world needs yet another editor! Rather, I see Vip as a proof of concept, and as a collection of examples of PicoLisp coding. And of course it is fun!

The "P" in "Vip" may mean "PicoLisp", but also "personalized". It does not even attempt to cover the full power of Vim, and is not intended as a replacement. But since then it is my default editor for PicoLisp code and e-mails, and of course for writing this article. So I should first report what it does not support: (and probably other things which I did not care about)

Simplicity was a major design goal, perhaps at the cost of efficiency. This may result in poor performance when editing large files with Vip.

On the other hand, there are some dedicated PicoLisp features not easily available in Vim (at least I could not figure out how to do them in vimscript): In any case, Vip supports most expected features of a usable Vim subset, including:

The Source Code

Vip is included in the PicoLisp distribution. It consists of the library in "@lib/vip.l" and an executable front-end "@bin/vip".

The library has grown a lot since the original version of this article, so the line count is more than 1000 by now.

It does not use any other PicoLisp libraries, only the picolisp binary and the lib.l base file.

The first versions used the Unix "ncurses" library, which is still the standard Unix way of character screen handling. But as curses turned out to be a mess, it now uses only direct ANSI escape sequences (VT-100).

If a file ~/.pil/viprc exists, it is 'load'ed as a config file. It may contain arbitrary configurations and extensions.

Vip defines two classes for buffers and windows,
   (class +Buffer)
   (class +Window)
but uses them in a somewhat unconventional, non-OOP, way. This is because these classes are final, and neither inheritance nor polymorphism is needed. Only few methods are defined, just normal functions operating implicitly on This, the current window.

A +Buffer holds the edited text in the text property as a list of lists of characters. Each character is a transient symbol, storing markup information in its value.

The markup is generated by the markup function. This function scans the whole text (a relatively expensive operation) using a 'finite state' machine. As a result, each character is known to be either normal text, the member of a string, or of a comment of a certain nesting depth. This information is used later for highlighting strings (with underlines) and comments (cyan color), and to handle indentation and matching parentheses.

A +Window displays the contents of a buffer. There may be multiple windows on a buffer, but no window without a buffer. Each window is linked to its neighbors with the prev and next properties.

The bottom window, which is created implicitly with a default size of one line, is called the "command window" (stored in the *CmdWin global). It can get permanent focus with normal window-change commands (e.g. qj or ^Wj), or temporary focus with commands like :, !, / or SPACE in a text window.

All windows except the command window have a status line at the bottom. It displays (from left to right) If the path name is too long, the status toggles between path-only and the other informations upon each move or edit operation.

The goto function is the main workhorse to move around (jump, scroll) in the buffer and to display its contents.

getch waits for - and returns - key-presses (NIL if Escape was hit).

Modifications of the text go through change, undo and redo, to keep track of the undo and redo stacks for each buffer. The changes are done with a mixture of destructive and non-destructive list operations, to keep the amount of garbage small, yet still have data fragments which are shareable across the undo/redo steps. Usually this is achieved by destructively modifying a single cell in the top level list, saving its CAR and CDR for undo/redo.

Movement functions like goLeft, goFind or word are the primitives to be combined in move with change, delete, yank and external filters, optionally with a count and other informations. It is important that they are usually never executed directly, but used to build - sometimes with the help of 'curried' functions - executable expressions which are indeed executed, but also kept in the *Repeat global to be repeatable with the . dot command.

command interprets and executes text in the command window, and the long loop at the end of the source is the main program. Start with inspecting the case statements to find out available commands, and how Vip handles them.

Running Vip

To run Vip on your machine, three conditions must be satisfied:

02aug19   abu