retroforth/example/edit.forth
crc fcbfebb54e some small work on edit.forth
FossilOrigin-Name: 1dbd50b3cfe48b522714712d040af801c2b6905fee761e1e24580f2bcc1506fc
2019-02-02 04:45:38 +00:00

350 lines
8.6 KiB
Forth
Executable file

#!/usr/bin/env retro
# Hua: a text editor written in RETRO
Hua is a small, functional text editor written in RETRO, using
the *RRE* interface. It is line oriented, visual, and tries to
be very simple to use.
## 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 if or
when needed.
## The Code
Since this runs as a standalone application I use a quick check
to exit if no arguments were passed.
~~~
sys:argc n:zero? [ #0 unix:exit ] if
~~~
If I get here, a filename was provided. So I start by creating
a few variables and constants.
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.
~~~
#80 'COLS const
#16 'MAX-LINES const
'/tmp/rre.edit 'TEMP-FILE s:const
~~~
Next are the variables that I use to track various bits of
state.
~~~
'SourceFile var
'CurrentLine var
'LineCount var
'ShowEOL var
'FID var
'CopiedLine d:create #1025 allot
~~~
Get the name of the file to edit.
~~~
#0 sys:argv s:keep !SourceFile
~~~
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 sys:argv 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.
~~~
:---- (-)
COLS [ $- c:put ] times 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? (n-nf)
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? [ ASCII:LF @FID file:write ] if file:s:put n:inc ] process-lines ;
~~~
Replacing a line is next. Much like the `delete-line`, this writes all
but the current line to a dummy file. It uses a `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 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.
~~~
: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
$g [ goto constrain ] case
$q [ 'stty_-cbreak unix:system #0 unix:exit ] case
drop ;
:edit
'stty_cbreak unix:system
repeat
display help handler
again ;
~~~
Run the editor.
~~~
edit
~~~