retroforth/example/retro-edit.retro
crc 6f8bd3047f nga-c: non-libc version updated. closes #59
FossilOrigin-Name: e52d8d1976ce3be3dbdc10e92667e629602ae91a8619213a8ff8cc642524ddb0
2021-06-17 13:58:50 +00:00

411 lines
10 KiB
Forth
Executable file

#!/usr/bin/env retro
# retro-edit
Copyright (c) 2021, Charles Childers
As part of my personal goal of using my own tools as much as possible,
I've written this little line editor. It's vaguely similar to ed or
edlin, but I've not attempted to replicate these. Mine is much simpler.
On startup, it reads each line from a file into memory. The maximum line
length and number of lines can be configured in the source, but must fit
into Retro's memory space.
Commands are entered as a single character, which may be followed by
optional or mandatory parameters.
An editing session (sans output) might look like:
,n
a5
i5,enter some text on line 5
p0,10
x8
p15,30
x20,22
,n
w
q
Look further in the source for a table of commands.
# The Code
## Configuration
On startup, this loads a working file specified here. Use the
commands later to load a specific file if needed.
~~~
'scratch 'SCRATCH s:const
~~~
The editor needs a max line length and max number of lines per
file.
~~~
#141 'cfg:MAX-LINE-LENGTH const
#2001 'cfg:MAX-LINES const
~~~
## The File Contents
The file gets read into an array of lines. This is in the `File`
structure. `Lines` holds the number of lines in the file.
A word, `ed:to-line` takes a line number and returns the address
of the actual line contents.
Of note here: these account for adding in a NULL terminator for
each line.
~~~
'File d:create
cfg:MAX-LINES cfg:MAX-LINE-LENGTH *
cfg:MAX-LINES + allot
'Lines var
'Filename d:create
#1025 allot
:ed:constrain (n-m) #0 @Lines n:limit ;
:ed:to-line (n-a) cfg:MAX-LINE-LENGTH over * + &File + ;
~~~
## Display A Line
Line display is trivial in this. I optionally support line numbers,
controlled by setting `ShowLineNumbers` to `TRUE`.
~~~
TRUE 'ShowLineNumbers var-n
:ed:show-line-number (-)
@ShowLineNumbers [ dup n:put ': s:put tab ] if ;
:ed:display-line (n)
ed:constrain ed:show-line-number ed:to-line s:put nl ;
~~~
## Command Processor
Commands are single characters. I reserve an array of pointers
(`Commands`), with the ASCII value of the character being an
index into this. If the final pointer is non-zero, this will
call the command handler.
`ed:register-command` is used to add a handler to the table,
and `ed:deregister-command` is used to remove one.
~~~
'Commands d:create #255 allot
:ed:register-command (ac) &Commands + store ;
:ed:deregister-command (c) &Commands + v:off ;
:ed:process-command (c) fetch &Commands + fetch 0; call ;
~~~
## Some Editing Functions
~~~
:ed:blank-line (n) ed:to-line s:empty swap s:copy ;
:ed:delete-line (n)
&Lines v:dec
@Lines over - [ [ ed:blank-line ]
[ dup n:inc swap [ ed:to-line ] bi@ s:copy ]
[ n:inc ] tri ] times drop ;
:ed:copy-line (mn) [ ed:to-line ] bi@ s:copy ;
{{
:shift-line dup n:dec swap ed:copy-line ;
---reveal---
:ed:insert-line (n)
[ @Lines [ [ shift-line ] sip n:dec dup-pair eq? ] until drop
&Lines v:inc ] sip ed:blank-line ;
}}
~~~
## Input
Input is read by `ed:get-input`. This returns the first character as
a value and stores the rest, with the pointer being kept in `Input`.
~~~
'Input var
:ed:get-input (-c) s:get dup n:inc s:keep !Input ;
~~~
## Editor Loop
The editor loop is thus simple. Get input, process the command, and
repeat. I run this in a simple `Heap` preserving loop, so command
handlers can allocate space at `here` without worrying about cleanup
afterwards. I also reset the stack.
~~~
'Done var
'Context var
:edit
[ &Heap [
@Context [ @Context call ] if
reset ed:get-input ed:process-command
] v:preserve @Done ] until ;
~~~
## The Commands
In general, each command is intended to do a single task.
### Display
`re` displays the file contents on command. I provide a few commands for
this.
| , | | display all lines in the file |
| , | n | display all lines in the file with line numbers |
| p | line | display a single line |
| p | first,last | display a range of lines |
| / | | search for text; display matching lines |
| # | | toggle display of line numbers |
The `,` command displays all lines in the file. It will optionally
display line numbers if a `n` is included after the `,`.
~~~
:cmd:,
&ShowLineNumbers [
@Input 'n s:eq? [ &ShowLineNumbers v:on ] if
#0 @Lines [ dup ed:display-line n:inc ] times drop
] v:preserve ;
~~~
It's sometimes useful to have all outputs using line numbers. I
define a `#` command to toggle the line number display. This will
affect all outputs.
~~~
:cmd:# @ShowLineNumbers not !ShowLineNumbers ;
~~~
To display a single line, use `p` followed by the line number or to
display a range of lines, use `p`, followed by a `first,last` line
number pair.
~~~
:pair? @Input $, s:contains/char? ;
:get-limits @Input $, s:tokenize [ s:to-number ed:constrain ] a:for-each ;
:display-range over - n:inc [ dup ed:display-line n:inc ] times drop ;
:cmd:p
pair? [ get-limits display-range ]
[ @Input s:to-number ed:display-line ] choose ;
~~~
The final display related command is `/`, which displays lines that contain
the text following the `/`.
~~~
:match? I ed:to-line @Input s:contains/string? ;
:cmd:/ @Lines [ match? [ I ed:display-line ] if ] indexed-times ;
~~~
### Editing
| a | line | add a line at line number, shifting lines down |
| a | line,count | add lines at line number, shifting lines down |
Adding lines is done with `a`. Provide either a line number or a
`first,count` pair and lines will be inserted starting at the specified
line.
~~~
:cmd:a
pair? [ get-limits [ dup ed:insert-line ] times drop ]
[ @Input s:to-number ed:insert-line ] choose ;
~~~
| x | line | remove line |
| x | first,last | remove lines first through last, inclusive |
Use `x` to delete one or more lines. Pass either a single line number or
a `first,last` pair.
~~~
:delete-lines [ dup ed:delete-line n:dec dup-pair lteq? ] while drop-pair ;
:cmd:x
pair? [ get-limits delete-lines ]
[ @Input s:to-number ed:delete-line ] choose ;
~~~
| d | line | erase contents of a line |
To erase the contents of a line, use `d` followed by the line number.
~~~
:cmd:d @Input s:to-number ed:blank-line ;
~~~
| i | line,text | replace contents of line with text |
| e | line | insert text beginning at line, shifting down |
| y | line,text | append text to end of line |
I provide two words for editing the text of the file. `i` replaces the
text on a line with the provided text, and `e` insterts text, shifting
existing lines down. When entering text with `e`, use a period on an
otherwise blank line to return to the command processing mode.
~~~
:cmd:i @Input $, s:split/char s:to-number ed:to-line [ n:inc ] dip s:copy ;
{{
:add-space dup ed:insert-line ;
:store-line over ed:to-line s:copy n:inc ;
:cleanup n:dec ed:delete-line ;
:ruler #6 [ '---------+ s:put ] times '---- s:put nl ;
---reveal---
'Prompt var
:cmd:e
ruler
@Input s:to-number
[ @Prompt [ @Prompt call ] if
add-space s:get [ store-line ] sip '. s:eq? ] until cleanup ;
}}
:cmd:y
@Input $, s:split/char
s:to-number ed:to-line [ [ n:inc ] dip s:prepend ] sip s:copy ;
:cmd:Y
@Input $, s:split/char
s:to-number ed:to-line [ [ n:inc ] dip s:append ] sip s:copy ;
~~~
Copy/paste
~~~
'LineBuffer d:create
cfg:MAX-LINE-LENGTH allot #0 ,
:cmd:c @Input s:to-number ed:to-line &LineBuffer s:copy ;
:cmd:u cmd:a &LineBuffer @Input s:to-number ed:to-line s:copy ;
~~~
### Setting the Filename
| f | filename | set the filename for saves, loads |
~~~
:cmd:f @Input &Filename s:copy ;
~~~
### Saving
| w | | save file |
Files are saved with `w`. The number of lines written will be displayed
upon completion.
~~~
{{
:with-file &Filename file:open-for-writing [ swap call ] sip file:close ;
:file:nl ASCII:LF over file:write ;
:write-line I ed:to-line [ over file:write ] s:for-each file:nl ;
:write-file [ @Lines [ write-line ] indexed-times ] with-file ;
:select @Input s:length n:-zero? [ cmd:f ] if ;
---reveal---
:cmd:w select write-file @Lines n:put nl ;
}}
~~~
~~~
:create-if-not-present
@Input s:length n:-zero? [ cmd:f ] if
&Filename file:exists? [ &Filename file:W file:open file:close ] -if ;
:erase-all
#0 cfg:MAX-LINES [ #0 over ed:to-line store n:inc ] times drop ;
:load-file
create-if-not-present
#0 &Filename [ over ed:to-line s:copy n:inc ] file:for-each-line
!Lines ;
:cmd:l erase-all load-file @Lines n:put nl ;
~~~
### New
~~~
:cmd:* SCRATCH &Filename s:copy erase-all #1 !Lines ;
~~~
### Quit
| q | | quit retro-edit |
Use `q` to quit the editor.
~~~
:cmd:q nl &Done v:on ;
~~~
### Others
| n | line | indent line |
| N | line | unindent line |
| ; | | save, then run file via retro |
| ; | text | save, then run text as a shell command |
~~~
:cmd:n @Input s:to-number ed:to-line dup '__ s:prepend swap s:copy ;
:cmd:N @Input s:to-number ed:to-line dup #2 + swap s:copy ;
:cmd::
@Input dup s:length n:zero?
[ drop &Filename ] if 'retro_%s s:format unix:system ;
~~~
## Register The Commands
~~~
{ ',p#/axdieyYfwlqnN:*cu [ ] s:for-each }
[ [ 'cmd:%c s:format d:lookup d:xt fetch ] sip ed:register-command ]
a:for-each
~~~
The final startup process is:
- copy the file name to Filename
- create it if the file does not exist
- load the file into memory
- run the editor command loop
~~~
:cmd:?
here n:put nl ;
&cmd:? $? ed:register-command
~~~
~~~
s:empty !Input erase-all
script:arguments n:-zero? [ #0 script:get-argument ] [ SCRATCH ] choose
&Filename s:copy
load-file
edit bye
~~~
{ '|\_|V|_/|\_|\_/\__Charles_Childers'_Personal_Computing_System
'|/_|_|__|__|/_\/__a_Unix_Kernel,_Retro_Forth,_and_a_non-POSIX
'|\_|_|__|__|\_/\__environment_written_in_Forth }