You can use line editing on gosh REPL if the terminal is capable. The feature is implemented entirely in Gauche itself, instead of relying on external libraries such as libreadline. This document describes the internals of the line editing feature for those who want to fix bugs or add new features.
There are number of modules involved in this feature.
-
gauche.interactive.editable-reader
: This module is autoloaded whengosh
is invoked in the interactive mode. First it tries to obtain a default console (text.console
), and if it succeeds, create a line editor instance (text.line-edit
) and returns several procedures to be called fromread-eval-print-loop
. If it can’t get a default console (e.g. the terminal is not capable, or the stdios are connected to non-terminal device), then it returns false values andread-eval-print-loop
falls back to a basic I/O without editing capability. This module also calls load/save edit history API in the appropriate time. -
text.console
: This module provides an abstract API to control consoles such as cursor placements. Currently it supports VT100-compatible console and Windows console. It used to be a standard to use termcap/terminfo to abstract the device difference, and curses for high-level console manipulations. However, the world has moved on. We no longer have wide variations of terminal controls, so that adding dependencies to those external libraries is overkill. Besides, Windows console requires totally different controls than the traditional Unix consoles. So we decided to roll our own. -
text.line-edit
: This module effectively implements a text editor. It usestext.gap-buffer
for the buffer structure anddata.ring-buffer
for the kill ring. It usestext.console
for drawing, so it is independent from the actual console device. It also supports unlimited undoing, and save/load histories. -
text.gap-buffer
: Implements a gap buffer, suitable for the back-end of text editor.
The main program of gosh
calls user#read-eval-print-loop
if no
script file is given. If -q
option is given to gosh
, it is
gauche#read-eval-print-loop
(defined in libeval.scm
), which only
provides very basic REPL. However, if -q
option isn’t given,
gauche.interactive
is loaded first which shadows read-eval-print-loop
,
so that gauche.interactive#read-eval-print-loop
is invoked instead.
While loading gauche.interactive
, make-editable-reader
of
gauche.inetractive.editable-reader
is invoked. It tries to get
a default console by text.console#make-default-console
. If it can,
it returns four values - A procedure to read an S-expr, to read a line,
a thunk to skip trailing whitespaces, and <line-edit-context>
.
The returned readers are line-edit enabled.
The first three procedures are passed to gauche.interactive#make-repl-reader
,
which returns a reader that (1) recognizes top-level REPL commands,
and (2) consume trailing whitespaces after S-expression input.
If line editing is not enabled (by -fno-line-edit
option, or from
an environment variable), or make-editable-reader
can’t obtain
a default console, a repl reader is created with
(make-repl-reader read read-line consume-trailing-whitespaces)
.
Thus, it recognizes the top level REPL commands, but does not use
line editing.
The gauche.interactive#read-eval-print-loop
needs a reader, an
evaluator, a printer, and a prompter. Reader and prompter
are constructed as follows.
-
If line editing is enabled:
-
Prompter actually does nothing, since the line editor needs to know the columns used by the prompt.
-
Reader invokes
text.line-edit#read-line/edit
, which set the console to 'rare' mode, query the screen size and the current cursor location, show the prompt, and goes into editor mode.
-
-
If line editing is disabled:
-
Prompter displays the prompt.
-
Reader reads from the current input port, and handles the top-level REPL command and trailing whitespaces if needed.
-
An instance of <line-edit-context>
maintains the state of line editing.
It is coupled with an instance of <console>
; while line editing is on,
all console I/O should be done through <line-edit-context>
, or the
state it keeps and the actual content displayed on terminal become
out of sync.
Line editing is on when you call read-line/edit
with an a line-edit
context. It changes the console mode to monitor keystrokes, then
interacts with the user until they "commit" the input, then changes
the console mode back to the original mode and returns the text
the user committed.
The context has a table to map keystrokes to edit commands. A new context gets a copy of the default map. You can modify the map of a context without affecting other contexts.
An edit command is a procedure that takes three arguments, a context, a gap-buffer, and a keystroke object. It may change the content of the buffer, and must return one of the following objects. In general, edit commands shouldn’t directly change the state of the context.
-
'visible : the command changed something visible so we need to redisplay, but we don’t need to save the change in the actual buffer. The selection is cleared.
-
'unchanged : no change visually and internally; we break undo sequence and reset last yank, but not clear selection. This occurs, for example, backward-char at the beginning of the input.
-
'nop : totally ignore the key input.
-
'redraw : we won’t change the state of the editor, but we reobtain the cursor position before redrawing the buffer content. This is necessary when the command wrote something on the screen and we don’t want to clobber it.
-
'moved : the command only moved cursor pos. requires redisplay, but we keep selection.
-
#<eof> : end of input - either input port is closed, or the user typed EOT char when the buffer is empty.
-
'commit : record the current buffer to the history, and returns it.
-
'undone : the command undid the change. we record the fact, for the consecutive undo behaves differently than other commands.
-
<edit-directive> : the command changed the buffer contents, and the return value is an edit directive to undo the change.
-
(yanked <edit-directive>) : this is only returned by yank and yank-pop command.
…(more to come)…