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 one month 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 a little ore than 1000 by now.

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

It does use, however, the Unix "ncurses" library, which is still the standard Unix way of character screen handling. This is done via 'native' function calls:
   (de curses @
      (pass native "") )
Then it initializes the ncurses environment and remembers the screen dimensions in the global variables *Lines and *Columns.

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. Almost no 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) as a finite state machine, using the 'state' function. 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 focus with normal window-change commands (e.g. qj or ^Wj), with commands like :, ! or /, or by hitting ENTER in a text window.

All windows except the command window have a status line at the bottom. It displays 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. KEY_RESIZE is a special event which is returned when the terminal size was changed.

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 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: The UTF-8 locale is a general PicoLisp requirement. Due to the native calls Vip runs only on pil64, and it needs at least picoLisp-16.12.

picoLisp-16.12 is currently in Debian Testing ("stretch").

If you have a global installation of 64-bit PicoLisp on your machine, you can just move the file vip into some directory in your execution $PATH, and you are done.

Alternatively, you can install new PicoLisp locally, and put symbolic links into /usr/bin and /usr/lib as described in INSTALL or adjust the hash-bang in the first line to point to your local installation.

Ncurses turned out to be a major headache. It is surprising how inconsistent and badly maintained this veteran Unix library comes along. See, for example, the paragraph starting with "Fourth" on to get an impression.

Vip now dynamically searches for a usable ncurses shared library at load time, so the situation is a bit easier now.

Window Management

To start Vip, call it with one (or more) files names:
   $ vip file.l
It will display the first file in the large main window above the single-line command window. If called without arguments, it opens an empty temporary file "".

or F6 you can open the next file(s), and with
or F5 you can step backwards through the files.

gf opens the file whose name is under the cursor. Similarly, K jumps to the definition of the symbol under the cursor (debug mode only, see below).

A backslash \ switches to the most recent buffer in this window, and if you prefix with a count, the corresponding buffer is selected.

As in my own .vimrc, I mapped "q" to "^W", because "^W" is very tedious to type.

So qs splits the current window into to halves, and you can scroll to other parts of the file, or open other files with :n or e <file etc.

qx exchanges the contents of two windows, qk changes the focus to the window above, and qj to the window below (possibly the command window).

qq closes the current window, or Vip if this was the last (non-command) window.

+ increases the size of the current window, and - decreases it. = makes all windows of equal size.

Tag Stack

^] open the source of the symbol under the cursor in the current window. Note that this works only when Vip was started in debug mode:
   $ vip file1.l file2.l +
This feature makes perhaps more sense when Vip is loaded as a library while debugging other programs.

With ^T the tag stack can be popped, going back to the previous location.

Note that this "tag" functionality does not use a tag file. Instead, the debug properties of the symbols are used directly.

Differences to Vim

There are some deliberate - though not mission-critical - differences in behavior between Vim and Vip. Some of them are because I prefer them this way, and some as a consequence of the implementation.

One is the w word motion command. The normal Vi/Vim philosophy is that "change", "delete" etc. operate on the range specified by the motion. w, however, behaves differently in Vi/Vim for moves (going to the beginning of the next word) and changes (going only to the end of the current word).

To keep Vip simple, and also for reasons of consistency, a command like cw or dw will change the text up to including the first character of the next word. This is usually not what you want. Instead, use ce (change to end of word) or de.

Similar differences may also exist for other motion commands. As a rule, assume that Vip simply does what follows from the motion logic.

When undoing a change operation, it often requires 2 presses of u, because the removal of the old and the insertion of the new text are stored internally as separate steps. This is a bit odd at first, but turns out useful in certain situations.

Not really a difference is the behavior of the left and right arrow keys. For that I have map <LEFT> zh and map <RIGHT> zl in ".vimrc".

Another difference is the behavior of the command window.

The Command Window

Vip does not implement the legacy ex commands which are available in Vi/Vim.

Instead, we can consider the command window as a specialized "REL". Not a "REPL", a "read-eval-print" loop, but a "read-eval loop". It takes commands in special formats, and evaluates them.

In some cases, it behaves very similar to Vi. For example, pressing : in a text window will cause the focus to go to the command window, display a colon, and wait for a command. Currently only these commands are supported: For the ls command it may be useful to increase the size of the command window.

Similarly, pressing / or ? in a text window will cause the focus to go to the command window, and start a search. Note, however, that regular expressions are not supported. You can either search for a constant text (case sensitive), or use @ in the search pattern as a wildcard. In the latter case, results will match till the end of the line.

If you change the focus explicitly to the command window, write a Lisp expression (i.e. starting with an opening parenthesis), and press ENTER, this Lisp expression will be evaluated. This allows arbitrary editor commands to be maintained in the command window.

In any case, the command window can be edited just like any other window, to go back in the "history", and individual lines can be re-executed by pressing ENTER.

Other Commands

The , comma command is redefined in Vip to fix the indentation of a paragraph (i.e. until the next empty line) according to the PicoLisp rules (3 spaces per level). It uses the internal markup information to handle strings, (nested) comments and (super)parentheses.

F1 toggles the markup highlight display (strings, comments) on and off.

F2 shows the differences (changes) of the current file and a file with "-" appended to the name (see the bak and kab commands above).

F3 is similar, but calls a custom "dif" program. I use it with dedicated scripts which show the differences to files on other machines and/or in the incremental backups. The count prefix is used then to select the backup version.

F4 formats the current paragraph, calling the Unix "fmt" command.

07jul18   abu