retroforth/example/edit.retro
crc 660e52bdcb rename words in sys: to script: (old names still work in this release, but are deprecated)
FossilOrigin-Name: 1a43743f43076eb087ee4dd7fbfa96b8dfda2aa4ddcff41e6a5a9634ba6e239c
2020-09-14 19:55:02 +00:00

371 lines
9.1 KiB
Forth
Executable file

#!/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
~~~