660e52bdcb
FossilOrigin-Name: 1a43743f43076eb087ee4dd7fbfa96b8dfda2aa4ddcff41e6a5a9634ba6e239c
371 lines
9.1 KiB
Forth
Executable file
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
|
|
~~~
|