# Text Editor

This line based text editor lives at text/ed.fs and allows the examination and
modification of text contents. The Grid-based [io/grid] editor is [app/ed].

The central structure of this unit is EdBuf (editor buffer) and its API
is described below. There is initially one of such buffers in memory, but more
can be added. There will typically be only one active edbuf at once and the
"edbuf" word points to it.

This structure has all the methods you need to load, save and modify text
contents from within it. The buffer is an array [mem/array] of Line
structures which themselves reference buffers in an arena allocator
[mem/arena]. Lines have a maximum length of ARENASZ bytes.

Interacting directly with the buffer structure gives you a maximum of
flexibility, but is a bit verbose. To make text operations more efficient,
shortcut words are provided. This collection of words is called the Convenience
Layer.

## Position

An important concept in the editor is the concept of "position", that is, a
number that uniquely identifies a line in the buffer as well as a character
within that line.

Position is represented as a single integer with the high 16 bits being the line
position (lpos) and the low 16 bits being the character position within the
line (cpos).

Line numbers and character numbers are 0-indexed.

In the documentation below, "cursor" is used as a synonym for Edbuf's current
position.

## Clipboard

Ed seamlessly integrates with the clipboard [text/clip] by providing edbuf's
"clipto" word to copy a range of the buffer to the clipboard.

## Multiple buffers

There can be more than one edbuf struct in memory at once. The "edbufs" variable
is a LL that holds all of the edbufs. You can add new ones, cycle through them,
all of that. See the API below.

## Position API

cpos ( pos -- cpos )
  Extract the character index from "pos". For example, if n=0, it means the
  cursor is at the beginning of the line.

lpos ( pos -- lpos )
  Extract the line number from "pos".

joinpos ( lpos cpos -- pos )
  Merge lpos and cpos in a single pos value.

## Line API

The Line structure describes a line in the edbuf. The line structure does not
include the LF character. This means that an empty line has a cnt of 0.

We maintain a separate character count and allocated size. It's possible,
through deletion, that these two numbers differ. When inserting or appending
contents to the line, if we have enough space, we don't needlessly reallocate in
the arena buffer.

### Structures

struct Line {
  uint ptr ;
  ushort cnt allocsz ;
}

ptr     Pointer to the first character of the line
cnt     Number of characters in the line
allocsz Bytes allocated at ptr

### Constants

GRANULARITY  16  we allocate lines in chunks of that size

### Words

line[] ( line -- a u )
  Return the range corresponding to this line

printline ( line -- )
  Call "rtype" with the line's range.

?realloc ( arena n line -- )
  Verify whether "n" characters could be added to the line. If not, reallocate
  a sufficiently big buffer in "arena" to accomodate those "n" new characters.

append ( a u line -- )
  Assuming that there's enough space (aborts if not), append range "a u" at the
  end of the line.

split ( idx line -- a u )
  Truncate line at "idx" and yield its right part as range "a u".

words[] ( a u -- ... n )
  Yield a list of indexes of word boundaries contained in the specified range,
  that is, a non-whitespace character preceded by a whitespace character (or
  beginning of line).

  Highest index is on top of the list.

wordunder ( a u idx -- a u )
  Using words[], yield the subrange of the word under position "idx" in input
  range. This range includes trailing whitespaces.

## Edbuf API

The edbuf is mainly an array of line structures with various metadata attached
to them. The line structures themselves don't hold actual contents, that content
is held in the arena allocator that the edbuf manages.

Among edbuf's metadata, a very important one is the position, that is, where we
currently "are" in the buffer. That information is held by [io/stream]'s "pos".
At all times, that position remain "bounded" at the character level, that is,
whenever we select a new line, we ensure that the "cpos" part of the position is
within bounds of that line.

Edbuf extends the Stream [io/stream], allowing some pretty nice interactions
with other stream-aware words. How it works is rather simple: reading and
writing happen at current position which then advances the position by the
stream operation size.

When writing, whenever a LF character is encountered, we insert a new line after
the selected one and move current position at the beginning of that empty line.

Therefore, "loading" a file contents to the buffer is a write to it. Saving the
buffer somewhere else is a read from it.

No special care is given to ASCII control characters except for LF. Try to avoid
them in your text files.

### Structures

extends Stream struct EdBuf {
  uint buf lines ;
  \ for app/ed
  uint top gutter visualmode mark ;
  [uchar,0] filename ;
}

buf        An arena buffer with the contents of lines
lines      Array of Line structures
top        Line index of the top of the screen. Used in [app/ed]
gutter     Position of the gutter, default 80. See [app/ed]
visualmode Whether the visual mode is on for this buffer
mark       Position where visual mode is anchored (has started at)
filename   String buffer containing the path associated to the buffer

### Line management words

line ( idx ed -- line )
  Yield the line struct at index "idx".

curline ( ed -- line )
  Yield line struct of current position.

empty ( ed -- )
  Empty the buffer, resetting positions and arena allocator.

empty? ( ed -- f )
  Yields whether "ed" is empty (contains a single empty line).

bounds ( pos ed -- pos )
  Ensure that "pos" is valid within the buffer. If its line index exceeds the
  buffer line count, target the last line, then on that line, ensure that the
  character index doesn't exceed the length of the line.

godown ( n ed -- )
  Move position down by "n" lines.

goup ( n ed -- )
  Move position up by "n" lines.

go ( n ed -- )
  Move position to line number "n".

goleft ( n ed -- )
  Move position left by "n" characters, not crossing line boundaries.

goright ( n ed -- )
  Move position right by "n" characters, not crossing line boundaries.

nextword ( ed -- )
  If positioned on a whitespace, advance it to the next non-whitespace character
  on the same line, if any. If positioned on a non-whitespace, advance to the
  next whitespace, then to the next non-whitespace.

prevword ( ed -- )
  Same as nextword, but scanning leftwards.

edfind ( str ed -- )
  Find occurence of "str" in buffer, starting from current position. If found,
  position is moved to that occurence. If not, position stays unchanged.

edfindnext ( ed -- )
  Repeat the last edfind.

cprint ( ed -- )
  Print current line with a second line under it containing a "^" character
  indicating the current "cpos".

linecnt ( ed -- cnt )
  Return total line count of the buffer.

delchars ( n ed -- )
  Delete "n" characters starting from current position to the right, not
  crossing line boundaries.

replchar ( c ed -- )
  Replace character at current position with "c".

dellines ( n ed -- )
  Delete "n" lines starting from "lpos".

clipto ( pos ed -- )
  Copy the contents between current position and "pos", including newlines, to
  the clipboard.

delto ( pos ed -- )
  Delete contents between current position and "pos", even across multiple
  lines. If the text to delete doesn't span whole lines, the "before" part of
  the first line is joined with the "after" part of the last line.

appendline ( ed -- )
  Append a new empty line after selection.

insertline ( ed -- )
  Insert a new empty line before selection.

wordundercursor ( ed -- a u )
  Move position to the first character of the word under it, or to the beginning
  of the line, whichever comes first. Then, find the next whitespace that follow
  that new position and yield the range that represents the resulting word. Can
  yield a null range.

### Buffer management words

edbufs ( -- a )
  LL of edbufs, the LL structure being a 4b LL link followed by a 4b pointer to
  an edbuf struct. This LL is never empty.

curbuf ( -- a )
  Pointer to the LL representing the currently "active" struct.

edbuf ( -- ed )
  Yields the ed struct associated to "curbuf".

newedbuf ( -- ed )
  Create a new edbuf structure with its own "buf" arena.

addedbuf ( -- )
  Create a new edbuf and add it at the end of "edbufs", making the new edbuf
  the current one.

nextedbuf ( -- )
  Set "curbuf" to the next element of the LL. If none, cycle back to the
  beginning.

findedbuf ( strpath -- f )
  Search edbufs for one that is associated to "strpath". If found, that buffer
  becomes the current one and f=1. Otherwise, f=0.

findemptybuf ( -- f )
  Look for an empty buffer in "edbufs" and, if found, make it the "curbuf" and
  yield f=1. Otherwise, f=0.

?addedbuf ( -- )
  If an empty edbuf is available, make it active. Otherwise, add a new edbuf and
  make it active.

.edbuf ( ed -- )
  Print a short one-line description of the edbuf, that is, its associated
  filename, its line count, and a "*" if it's the currently active buffer.

.edbufs ( -- )
  Print descriptions of all edbufs in "edbufs".

## Convenience layer

The convenience layer is a collection of words with short names that makes
interacting with the API above easier.

The "input" words ("I", "o", "O") work in a peculiar way because they consume
the rest of the input line. For example, let's say that you type this:

  foo bar I Hello there

"foo", "bar" and "I" will be interpreted normally, but "Hello there" will be
copied without going through the interpreter. The content is then sent to the
edbuf as is. The LF character is not sent (otherwise, every insert operation
would result in a new line being inserted).

"I" is used instead of "i" to avoid shadowing the do..loop variable.

### Values

pagesz   Number of lines in a "page". Default 20

### Words

p ( -- )
  Print one "page".

s ( -- )
  Print "stats", that is, the current line with :cprint followed by another
  line with "current lineno" / "line count".

I ( "...\n" -- )
  Insert typed line before the current one and select it.

o ( "...\n" -- )
  Append a line after the current one with the typed contents.

O ( "...\n" -- )
  Insert a line before the current one with the typed contents.

f ( "...\n" -- )
  Find typed string in buffer.

n ( -- )
  Repeat the last find

dl ( n -- )
  Delete "n" lines starting from current pos.

dc ( n -- )
  Delete "n" characters starting from current pos.

g ( lineno -- )
  Select line number "lineno"

c- ( n -- )
  Go "n" characters to the left.

bol ( -- )
  Go to the beginning of the line.

c+ ( n -- )
  Go "n" characters to the right.

eol ( -- )
  Go to the end of the line.

l- ( n -- )
  Go "n" lines up.

l+ ( n -- )
  Go "n" lines down.

eof ( -- )
  Go to the last line of the buffer.

sl ( -- )
  If the line is longer than "gutter" characters, split line at cursor, that is,
  append a line under the cursor, truncate the current line at the cursor and
  move the truncated contents to the newly added line.

  Cursor is moved to the beginning of the created line.

jl ( -- )
  Join line at cursor with the line under it. That is, delete the line under
  the cursor and append the contents of the deleted line to the current line.

  Cursor is moved to join position (right between former lines).

edload ( strpath -- )
  Empty edbuf, open "path", associate the buffer to it, then load the contents
  of the opened file into the buffer.

edload<< ( "..." -- )
  Shorcut for "word edload".

?edload ( strpath -- )
  Try "findedbuf" on "strpath". If it yields 0, make an empty edbuf active using
  "?addedbuf" and load "strpath" into it. In other words, this word always
  preserves existing buffers and never overwrite their contents.

?edload<< ( "..." -- )
  Shortcut for "word ?edload".

edsaveto ( file -- )
  Save the contents of edbuf to "file", truncating it if the new contents is
  smaller than the old one, then close the file.

edsave ( -- )
  Save contents of edbuf's associated filename, taking care of opening and then
  closing it. Errors out if there is no associated filename.
