#!/bin/sh

stty cbreak /usr/bin/env retro $0 $1 stty -cbreak exit


 _   _              | | | |_   _  __ _  | |_| | | | |/ _` | |  _  | |_| | (_| | |_| |_|\__,_|\__,_| line oriented editor

Hua: a text editor written in RETRO

Hua is a small, functional text editor written in RETRO for Unix systems. It is line oriented, visual, and easy to learn.

Starting

Hua is intended to run as a standalone tool. Use a line like:

edit.forth filename

To create a new file:

edit.forth new filename

A Word of Warning

Hua saves changes as you edit the file. I advise using it along with a version control system so you can revert changes when needed.

The Code

Since this runs as a standalone application I use a quick check to exit if no arguments were passed.

~~~script:arguments n:zero? [ 'No_file_specified! s:put nl #0 unix:exit ] if ~~~

If I get here, a filename was provided. So I start by creating a few variables and constants.

Get the name of the file to edit.

~~~#0 script:get-argument s:keep 'SourceFile var-n ~~~

The configuration here is for two items. The number of lines from the file to show on screen, and the name of the temporary file to use when editing.

~~~#70 'COLS      const #16 'MAX-LINES const '/tmp/hua.editor.scratch 'TEMP-FILE s:const ~~~

Next are the variables that I use to track various bits of state.

~~~'CurrentLine var 'LineCount   var 'ShowEOL     var 'FID         var 'CopiedLine  d:create #1025 allot ~~~

To create a new file, Hua allows for the use of new followed by the filename. I handle the file creation here.

~~~@SourceFile 'new s:eq?   [ #1 script:get-argument s:keep !SourceFile     @SourceFile file:A file:open file:close ] if ~~~

This is just a shortcut to make writing strings to the current file easier.

~~~:file:s:put (s-) [ @FID file:write ] s:for-each ASCII:LF @FID file:write ; ~~~

I now turn my attention to displaying the file. I am aiming for an interface like:

<filename> : <line-count> --------------------------------------------------------------- *   99:    100: :n:square dup * ;    101:    102: This is the current line    103: --------------------------------------------------------------- j: down | k: up | ... other helpful text ...

The denotes the currently selected line.

I start with words to count the number of lines in the file and advance to the currently selected line.

~~~:count-lines (-)   #0 @SourceFile [ drop n:inc ] file:for-each-line dup !LineCount ;   :skip-to   @CurrentLine MAX-LINES #2 / - #0 n:max [ @FID file:read-line drop ] times ; ~~~

Now for words to format the output. This should all be pretty clear in intent.

clear-display uses an ANSI/VT100 escape sequence. This might need to be adjusted for your chosen terminal.

~~~:clear-display (-)   ASCII:ESC dup '%c[2J%c[0;0H s:format s:put nl ; ~~~

This just displays the separator bars.

~~~:---- (-)   #7 [ $- c:put ] times     #0 COLS #10 / [ dup n:put #9 [ $- c:put ] times n:inc ] times n:put nl ;   ~~~

Next, a word to display the header. Currently just the name of the file being edited and the line count.

~~~:header (-)   count-lines @SourceFile '%s_:_%n_lines\n s:format s:put ; ~~~

The pad word is used to make sure line numbers are all the same width.

~~~:pad (n-n)   dup    #0 #9    n:between? [ '____ s:put ] if   dup   #10 #99   n:between? [ '___  s:put ] if   dup  #100 #999  n:between? [ '__   s:put ] if   dup #1000 #9999 n:between? [ '_    s:put ] if ; ~~~

A line has a form:

<indicator><number>: <text><eol>

The indicator is an asterisk, and visually marks the current line.

EOL is optional. If ShowEOL is TRUE, it'll display a ~ at the end of each line. This is useful when looking for trailing whitespace. The indicator can be toggled via the ~ key.

~~~:mark-if-current (n-n)   dup @CurrentLine eq? [ $* c:put ] [ sp ] choose ;     :line# (n-)   n:put ':_ s:put ;   :eol (-)   @ShowEOL [ $~ c:put ] if nl ;   :display-line (n-n)   dup @LineCount lt?   [ dup mark-if-current pad line# n:inc @FID file:read-line s:put eol ] if ;   :display (-)   @SourceFile file:R file:open !FID   clear-display header ---- skip-to   @CurrentLine MAX-LINES #2 / - #0 n:max count-lines MAX-LINES n:min   [ display-line ] times drop   ---- @FID file:close ; ~~~

With the code to display the file done, I can proceed to the words for handling editing.

I add a custom combinator, process-lines to iterate over the lines in the file. This takes a quote, and runs it once for each line in the file. The quote gets passed two values: a counter and a pointer to the current line in the file. The quote should consume the pointer an increment the counter. This also sets up FID as a pointer to the temporary file where changes can be written. The combinator will replace the original file after execution completes.

Additionally, I define a word named current? which returns TRUE if the specified line is the current one. This is just to aid in later readability.

~~~:process-lines (q-)   TEMP-FILE file:W file:open !FID   [ #0 @SourceFile ] dip file:for-each-line drop   @FID file:close   here TEMP-FILE file:slurp here @SourceFile file:spew ;   :current? (ns-nsf)   over @CurrentLine eq? ; ~~~

So first up, a word to delete all text in the current line.

~~~:delete-line (-)   [ current? [ drop '_ ] if file:s:put n:inc ] process-lines ; ~~~

Then a word to discard the current line, removing it from the file.

~~~:kill-line (-)   [ current? [ drop ] [ file:s:put ] choose n:inc ] process-lines ; ~~~

And the inverse, a word to inject a new line into the file.

~~~:add-line (-)     [ current? [ file:s:put ASCII:LF @FID file:write ]                [ file:s:put ] choose n:inc ] process-lines CurrentLine v:inc ; ~~~

Replacing a line is next. Much like the delete-line, this writes all but the current line to a dummy file. It uses an s:get word to read in the text to write instead of the original current line. When done, it replaces the original file with the dummy one.

~~~{{   :save (c-)     ASCII:BS  [ buffer:get drop ] case     ASCII:DEL [ buffer:get drop ] case     buffer:add ; ---reveal---   :s:get (-s)        s:empty [ buffer:set       [ repeat c:get dup ASCII:LF -eq? 0; drop save again ] call drop ] sip ; }}   :replace-line (-)     [ current? [ drop #7 [ ASCII:SPACE c:put ] times s:get ] if       file:s:put n:inc ] process-lines ; ~~~

The next four are just things I find useful. They allow me to indent, remove indention, trim trailing whitespace, and insert a code block delimiter at a single keystroke.

~~~:indent-line (-)   [ current? [ ASCII:SPACE dup @FID file:write @FID file:write ] if file:s:put n:inc ] process-lines ;   :dedent-line (-)   [ current? [ n:inc n:inc ] if file:s:put n:inc ] process-lines ;   :trim-trailing (-)   [ current? [ s:trim-right ] if file:s:put n:inc ] process-lines ;   :code-block (-)   [ current? [ drop '~~~ ] if file:s:put n:inc ] process-lines ; ~~~

And then a very limited form of copy/paste, which moves a copy of the current line into a CopiedLine buffer and back again. The line buffer is 1024 characters long, use of a longer line will cause problems.

~~~:copy-line (-)   [ current? [ dup &CopiedLine s:copy ] if file:s:put n:inc ] process-lines ;   :paste-line (-)   [ current? [ drop &CopiedLine ] if file:s:put n:inc ] process-lines ; ~~~

One more set of commands: jump to a particular line in the file, jump to the start or end of the file.

~~~:goto (-)   s:get s:to-number !CurrentLine ;   :goto-start (-)   #0 !CurrentLine ;   :goto-end (-)   @LineCount n:dec !CurrentLine ; ~~~

And now tie everything together. There's a key handler and a top level loop.

~~~:describe (cs-)   swap c:put $: c:put s:put ; :| describe '_|_ s:put ;   :help   $1 'replace_ |   $2 'insert__ |   $3 'trim____ |   $4 'erase___ |   $5 'delete__ |   $j 'down____ | nl   $k 'up______ |   $g 'goto____ |   $[ 'start___ |   $] 'end_____ |   $c 'copy____ |   $v 'paste___ | nl   $< 'dedent__ |   $> 'indent__ |   $~ 'mark_eol |   $| '~~~_____ |   '___________|_ s:put   $q 'quit____ | nl ; ~~~

~~~:constrain (-) &CurrentLine #0 @LineCount n:dec v:limit ; :handler     c:get       $1 [ replace-line                   ] case       $2 [ add-line                       ] case       $3 [ trim-trailing                  ] case       $4 [ delete-line                    ] case       $5 [ kill-line                      ] case       $~ [ @ShowEOL not !ShowEOL          ] case       $c [ copy-line                      ] case       $v [ paste-line                     ] case       $< [ dedent-line                    ] case       $> [ indent-line                    ] case       $| [ code-block                     ] case       $[ [ goto-start                     ] case       $] [ goto-end                       ] case       $j [ &CurrentLine v:inc constrain   ] case       $k [ &CurrentLine v:dec constrain   ] case       $h [ &CurrentLine v:inc constrain   ] case       $t [ &CurrentLine v:dec constrain   ] case       $g [ goto               constrain   ] case       $q [ bye                            ] case     drop ;   :edit   repeat     display help handler   again ; ~~~

Run the editor.

~~~edit ~~~