Version 2.0
A lightweight VI-compatible editor for CP/M 2.2 and CP/M 3.0, written in
HI-TECH C. Uses a gap buffer for efficient editing and ANSI escape sequences
for terminal control. Implements most of the basic movement and editing commands
including the . operator for repeat. Also has a single level undo.
Author: Juan Orlandini
License: MIT
- HI-TECH C Compiler for Z80/CP/M (V3.09 or later)
- CP/M 2.2 or CP/M 3.0 system with at least 48K TPA
cstart.as hvi.c hvi.h gap.c term.c screen.c emove.c
edit.c erepeat.c ex.c util.c cpmio.c
-
Copy all source files to a CP/M disk/drive.
-
Prepare
LX.LIB(one-time setup — removescsv.objto avoid symbol conflicts withcstart.as):
PIP LX.LIB=LIBC.LIB
LIBR d LX.LIB csv.obj
- Compile all source files:
C -CPM -O -C cstart.as
C -CPM -O -C hvi.c gap.c term.c screen.c emove.c edit.c erepeat.c ex.c util.c cpmio.c
- Link (the backslash
\continues the command past CP/M's 128-character line limit):
l
-Ptext=100H,data,bss -C100H -oh.com CSTART.OBJ CPMIO.OBJ UTIL.OBJ \
GAP.OBJ TERM.OBJ SCREEN.OBJ EMOVE.OBJ EREPEAT.OBJ EX.OBJ EDIT.OBJ HVI.OBJ LX.LIB
- Rename the output:
REN HVI.COM=H.COM
Note: The HI-TECH C linker command is
l(lowercase L). The-Ptext=100H,data,bssflag is required to place data and BSS sections correctly.cstart.asmust be first in the link order; it replaces the standardCRTCPM.OBJ.
If using the HI-TECH Z80 cross-compiler on a Unix host, use the same flags and
link order as above, then transfer hvi.com to your CP/M system via XMODEM,
ZMODEM, or disk image.
HVI [filename]
filename— file to open (created if it does not exist)
| Key | Action |
|---|---|
h / ← |
Move left one character |
l / → |
Move right one character |
j / ↓ |
Move down one line |
k / ↑ |
Move up one line |
Enter |
Move to first non-blank of next line |
w |
Forward to start of next word |
b |
Backward to start of previous word |
e |
Forward to end of word |
0 |
Move to beginning of line |
^ |
Move to first non-blank of line |
$ |
Move to end of line |
G |
Go to last line (or line N with count) |
gg |
Go to first line (or line N: 5gg) |
Ctrl-F |
Scroll forward one page; cursor lands in the middle of the new page. No-op if already at the end of the file. |
Ctrl-B |
Scroll backward one page; cursor lands in the middle of the new page. No-op if already at the beginning of the file. |
Ctrl-D |
Scroll forward half page |
Ctrl-U |
Scroll backward half page |
| Key | Action |
|---|---|
i |
Insert before cursor |
a |
Append after cursor |
I |
Insert at beginning of line |
A |
Append at end of line |
o |
Open new line below, enter insert mode |
O |
Open new line above, enter insert mode |
s |
Substitute character(s) (delete + insert) |
S |
Substitute entire line |
| Key | Action |
|---|---|
x |
Delete character under cursor |
X |
Delete character before cursor |
dd |
Delete current line |
dw |
Delete word forward |
db |
Delete word backward |
d$ |
Delete to end of line |
d0 |
Delete to beginning of line |
dG |
Delete to end of file |
D |
Delete to end of line (same as d$) |
cc |
Change current line |
cw |
Change word |
c$ |
Change to end of line |
C |
Change to end of line (same as c$) |
r |
Replace single character |
J |
Join line below to current line |
~ |
Toggle case of character |
| Key | Action |
|---|---|
yy |
Yank (copy) current line |
Y |
Yank current line (same as yy) |
yw |
Yank word |
y$ |
Yank to end of line |
p |
Put (paste) after cursor / below current line |
P |
Put before cursor / above current line |
| Key | Action |
|---|---|
/ |
Search forward for pattern |
? |
Search backward for pattern |
n |
Repeat last search |
N |
Repeat last search in reverse |
Search is case-insensitive plain substring (no regular expressions). In a large
file, search scans the entire file — not just the loaded buffer. When the match
is found in an unloaded section HVI reloads the window around it automatically.
search hit BOTTOM, continuing at TOP (or TOP … BOTTOM) is shown only when
the search genuinely wraps past the end (or beginning) of the file.
| Key | Action |
|---|---|
f{c} |
Move to next occurrence of character c on line |
F{c} |
Move to previous occurrence of character c on line |
; |
Repeat last f or F in the same direction |
, |
Repeat last f or F in the opposite direction |
Both repeat commands accept a count prefix (e.g. 3; skips to the third next match).
| Key | Action |
|---|---|
. |
Repeat last change |
u |
Undo last change |
: |
Enter ex command mode |
Ctrl-L |
Redraw screen |
| Key | Action |
|---|---|
| (any char) | Insert character |
Enter |
Insert newline |
Backspace |
Delete previous character |
Ctrl-H |
Delete previous character |
Ctrl-W |
Delete previous word |
Ctrl-U |
Delete to start of line |
↑↓←→ |
Move cursor (stay in insert mode) |
ESC |
Return to normal mode |
| Command | Action |
|---|---|
:w |
Write (save) current file |
:w filename |
Write to named file |
:q |
Quit (fails if unsaved changes) |
:q! |
Quit without saving |
:wq |
Write and quit |
:wq! |
Write and quit (force) |
:x |
Write if modified, then quit |
:x! |
Write and quit (force) |
:e filename |
Abandon current buffer and edit named file |
:e! filename |
Abandon modified buffer and edit named file |
:r filename |
Read file and insert after current line |
:N |
Go to line number N |
:$ |
Go to last line |
Most commands accept a numeric count prefix:
5j— move down 5 lines3w— move forward 3 words2dd— delete 2 lines10G— go to line 10
The status line shows:
"filename" [+]
[+]appears when the buffer has unsaved changes
Whenever HVI reads a chunk of a large file from disk it briefly shows
[Loading...] on the status line, replaced by the normal display once the
screen refreshes.
HVI can open and edit files that are larger than available RAM. The in-memory content is a sliding window: only a portion of the file is in memory at any time, and the window shifts as you scroll.
At startup HVI allocates the largest contiguous free block in the TPA (up to
BUF_MAX bytes of content) and loads as much of the file as fits. It records
where loading stopped (tail_offset) and the source filename (tail_file).
As you scroll forward with j, Ctrl-D, or Ctrl-F, HVI loads the next
4 KB chunk from disk automatically. When the buffer is full, the same number of
bytes are discarded from the beginning to make room. The discarded content is
always before the cursor so the cursor is never lost.
Scrolling backward with Ctrl-B reloads a window from an earlier position in
the file via a direct BDOS seek (no sequential scan needed).
Before shifting the window in either direction, HVI automatically saves all
in-memory edits to a swap file (HVISWP.TMP). Subsequent reads come from the
swap file, which contains the complete, up-to-date file content. This means
edits made at the beginning of a file are never lost when you scroll to the
end, and vice versa.
If the gap buffer fills up during editing, HVI saves to the swap file and reloads a smaller window around the cursor — you can keep typing without interruption.
/pattern and ?pattern search the entire file, not just the in-memory window.
The search proceeds in three phases:
- Scan the in-memory buffer from the cursor forward (or backward).
- If no unwrapped match is found, scan the unloaded file sections sequentially using direct BDOS reads — the tail (bytes after the buffer) for forward search, the head (bytes before the buffer) for backward search.
- When a match is found in an unloaded section, HVI reloads the window around it and places the cursor there.
The search hit BOTTOM, continuing at TOP message is shown only when the match
required crossing the true end-of-file boundary, not merely the buffer boundary.
A match in the unloaded tail of a forward search is reported without any wrap
message because it is genuinely ahead of the cursor in file order.
nG (e.g. 1000G) works correctly in large files. HVI scans the source file
from the beginning to locate the byte offset of line N, positions the window
there, and places the cursor precisely on that line. Navigation speed is
proportional to the line number, not the file size.
On every :w save, HVI reconstructs the full file:
- Bytes before the current window (from
tail_file) are copied verbatim. - The in-memory buffer is written in
CR+LFformat. - Bytes after the current window (from
tail_file) are appended verbatim. - A
Ctrl-Z(0x1A) EOF marker is written last.
When saving back to the same file that holds the unloaded portions, HVI writes
to HVITMP.TMP first, then renames, so the source is never overwritten before
it is fully read.
| File | Purpose |
|---|---|
HVISWP.TMP |
Swap file written before any window shift or buffer overflow |
HVITMP.TMP |
Intermediate used when saving back to the tail source file |
HVI uses ANSI/VT100 escape sequences. It defaults to 80 columns × 24 rows, which is the standard CP/M terminal size.
At startup HVI always queries the terminal for its actual dimensions by
sending ESC[999;999H (cursor to extreme bottom-right) followed by
ESC[6n (ANSI cursor-position report). The response is read byte-by-byte
via BIOS CONIN, bypassing BDOS canonical buffering. If the terminal does
not respond within the polling timeout, HVI silently falls back to the
80 × 24 defaults — it will not hang. No recompilation flag is needed.
Compatible terminals: VT100, VT220, xterm, ANSI.SYS, and most modern terminal emulators connected via a serial port.
HVI reads files in binary mode, stripping bare CR characters on load.
On save, each LF is written as CR+LF per CP/M convention, and the
file is terminated with Ctrl-Z (0x1A) per CP/M file format rules.
HVI is designed for 9600 baud serial terminals. All screen updates are sized to the minimum needed for the operation:
| Operation | Output sent |
|---|---|
h, l, 0, ^, $, f, F, ;, , |
Cursor reposition only — no text redrawn, no status bar update |
j, k, Enter, w, b, e, /, ?, n, N, gg, nG |
Terminal scroll + one new line (~53 bytes) when viewport shifts by one row, or cursor reposition only when no scroll needed — status bar not updated |
r replace character |
Single visual row redrawn |
x, X, D, ~, s, S, C |
Current logical line redrawn |
J, o, O, p, P, u, dw, dd, cw, Enter in insert mode |
Rows from cursor to bottom redrawn (rows above cursor skipped) |
G, Ctrl-F, Ctrl-B, Ctrl-D, Ctrl-U, : commands |
Full screen redrawn |
The "cursor to bottom" tier is the key optimization for editing operations: on a 24-row terminal with the cursor near the middle, it sends roughly half the bytes of a full screen refresh (~600 bytes vs ~1200 bytes at 9600 baud ≈ 0.5 seconds saved per keystroke).
The status bar is not refreshed on every j/k/Enter/nG keypress — it
updates on the next edit, search, page scroll, or Ctrl-L.
- Single-level undo only (
uundoes the most recent change) nGin a large file scans sequentially from byte 0 — navigating to line 1000 reads the first ~1000 lines from disk (fast); navigating to line 29000 in a 30K-line file reads most of the file (slow)- No visual/block selection mode
- No macro recording/playback
- No window splitting
- No regex search — plain substring match only (case-insensitive)
MIT License. Copyright (c) Juan Orlandini.