Line editor
A text of up to 30 lines edited with short commands - append, insert, replace, delete a line or a range, list, find - with undo and redo on two stacks. Every edit is one splice, and undoing it is the same splice reversed, so the stacks hold changes rather than copies of the text.
Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.
About
A text of up to 30 lines, edited with short commands: append a line, insert one before line N, replace line N, delete a line or a range, list all or part of the text, and find the lines that contain some text. Every edit can be undone and redone.
main.eml- the command line: reading a command, its checks, and what the screen showsbuffer.eml- the text and its history: the splice, undo and redo, and finding text
How each part works:
- Every edit is one splice: at one place in the text, some lines come out and some go in. Appending, inserting, replacing and deleting are all splices, and each is kept as a change - where, the lines that came out, the lines that went in, and a description such as
delete lines 2-4. - Undo and redo are two stacks of changes. Undoing a change is the same splice with the lines in and out swapped; the change then moves to the redo stack, and redoing it moves it back. A new edit empties the redo stack, as in most editors. The stacks hold changes, never copies of the whole text. They are lists popped by a slice rebuild, as in the corpus case
simple-stack. - A command is its first word, in any case (
aorappend, and so on); the text after the one space that follows the command - or the line number - is kept as typed, leading spaces included, so a line can be indented. - Finding ignores case. Lines are listed with their numbers, right-aligned.
What is checked: the text holds at most 30 lines of at most 60 characters; a new line can go in at 1 to one past the last line; a line to replace or delete, or a range (N-M, with N not after M), must exist. An unknown command says so and points to h; an empty command does nothing; u and y say when there is nothing to undo or redo.
Sessions: sessions/basic.in writes a four-line shopping list, inserts, replaces and deletes, undoes three times and redoes once, finds MILK, deletes a range and undoes it, appends an indented line (which empties the redo stack - the next y has nothing to redo), lists a range and shows the help; sessions/bad-input.in tries every command on an empty text, inserts past the end, gives a word for a line number, appends an empty line and a line of 61 characters, deletes a range that runs backwards and ranges that are not numbers, lists line 5 of one, searches for nothing and for text that is not there, fills the text to 30 lines, tries to add a 31st, replaces the last line, and types a command in capitals.
Built on the verified corpus case simple-stack (a list used as a stack, pushed by adding a one-item list and popped by a slice rebuild).
Recorded sessions
What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.
bad-input
interpreter: byte-equal== Line editor ==
A text of up to 30 lines of at most 60 characters. Commands:
a text append a line at the end
i N text insert a line before line N
r N text replace line N
d N, d N-M delete line N, or lines N to M
l, l N-M list the text, or lines N to M
f text find the lines that contain the text (any case)
u, y undo, redo
h, q this help, quit
edit (0 lines)>
edit (0 lines)> x
Unknown command 'x'; type h for help.
edit (0 lines)> l
The text is empty.
edit (0 lines)> u
Nothing to undo.
edit (0 lines)> y
Nothing to redo.
edit (0 lines)> d 1
The text is empty.
edit (0 lines)> r 1 new
The text is empty.
edit (0 lines)> i 2 first
A new line can go in at 1 to 1.
edit (0 lines)> i one two
Type a line number and the text, such as i 2 new text.
edit (0 lines)> a
Line 1 added.
edit (1 line)> l
1 |
edit (1 line)> a zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
That line has 61 characters; a line can have at most 60.
edit (1 line)> d 2-1
There is only line 1.
edit (1 line)> d x
Type a line number or a range, such as d 2 or d 2-4.
edit (1 line)> d 1-x
Type a line number or a range, such as d 2 or d 2-4.
edit (1 line)> l 5
There is only line 1.
edit (1 line)> l a-b
Type l, or a line number or a range, such as l 2-4.
edit (1 line)> f
Type the text to find, such as f cat.
edit (1 line)> f zzz
No line contains 'zzz'.
edit (1 line)> a line 2
Line 2 added.
edit (2 lines)> a line 3
Line 3 added.
edit (3 lines)> a line 4
Line 4 added.
edit (4 lines)> a line 5
Line 5 added.
edit (5 lines)> a line 6
Line 6 added.
edit (6 lines)> a line 7
Line 7 added.
edit (7 lines)> a line 8
Line 8 added.
edit (8 lines)> a line 9
Line 9 added.
edit (9 lines)> a line 10
Line 10 added.
edit (10 lines)> a line 11
Line 11 added.
edit (11 lines)> a line 12
Line 12 added.
edit (12 lines)> a line 13
Line 13 added.
edit (13 lines)> a line 14
Line 14 added.
edit (14 lines)> a line 15
Line 15 added.
edit (15 lines)> a line 16
Line 16 added.
edit (16 lines)> a line 17
Line 17 added.
edit (17 lines)> a line 18
Line 18 added.
edit (18 lines)> a line 19
Line 19 added.
edit (19 lines)> a line 20
Line 20 added.
edit (20 lines)> a line 21
Line 21 added.
edit (21 lines)> a line 22
Line 22 added.
edit (22 lines)> a line 23
Line 23 added.
edit (23 lines)> a line 24
Line 24 added.
edit (24 lines)> a line 25
Line 25 added.
edit (25 lines)> a line 26
Line 26 added.
edit (26 lines)> a line 27
Line 27 added.
edit (27 lines)> a line 28
Line 28 added.
edit (28 lines)> a line 29
Line 29 added.
edit (29 lines)> a line 30
Line 30 added.
edit (30 lines)> a one more
The text has 30 lines, the most it can hold.
edit (30 lines)> i 1 also no
The text has 30 lines, the most it can hold.
edit (30 lines)> r 30 the last line
Line 30 replaced.
edit (30 lines)> l 29-30
29 | line 29
30 | the last line
edit (30 lines)> APPEND
The text has 30 lines, the most it can hold.
edit (30 lines)> Q
Bye.
What was typed (54 lines)
x
l
u
y
d 1
r 1 new
i 2 first
i one two
a
l
a zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
d 2-1
d x
d 1-x
l 5
l a-b
f
f zzz
a line 2
a line 3
a line 4
a line 5
a line 6
a line 7
a line 8
a line 9
a line 10
a line 11
a line 12
a line 13
a line 14
a line 15
a line 16
a line 17
a line 18
a line 19
a line 20
a line 21
a line 22
a line 23
a line 24
a line 25
a line 26
a line 27
a line 28
a line 29
a line 30
a one more
i 1 also no
r 30 the last line
l 29-30
APPEND
Q
basic
interpreter: byte-equal== Line editor ==
A text of up to 30 lines of at most 60 characters. Commands:
a text append a line at the end
i N text insert a line before line N
r N text replace line N
d N, d N-M delete line N, or lines N to M
l, l N-M list the text, or lines N to M
f text find the lines that contain the text (any case)
u, y undo, redo
h, q this help, quit
edit (0 lines)> a Shopping list
Line 1 added.
edit (1 line)> a milk
Line 2 added.
edit (2 lines)> a eggs
Line 3 added.
edit (3 lines)> a bread
Line 4 added.
edit (4 lines)> l
1 | Shopping list
2 | milk
3 | eggs
4 | bread
edit (4 lines)> i 2 fruit:
Inserted as line 2.
edit (5 lines)> r 3 2 apples
Line 3 replaced.
edit (5 lines)> d 4
Deleted line 4.
edit (4 lines)> l
1 | Shopping list
2 | fruit:
3 | 2 apples
4 | bread
edit (4 lines)> u
Undid: delete line 4.
edit (5 lines)> l
1 | Shopping list
2 | fruit:
3 | 2 apples
4 | eggs
5 | bread
edit (5 lines)> u
Undid: replace line 3.
edit (5 lines)> u
Undid: insert line 2.
edit (4 lines)> y
Redid: insert line 2.
edit (5 lines)> l
1 | Shopping list
2 | fruit:
3 | milk
4 | eggs
5 | bread
edit (5 lines)> f MILK
1 line contains 'MILK':
3 | milk
edit (5 lines)> d 2-4
Deleted lines 2-4.
edit (2 lines)> l
1 | Shopping list
2 | bread
edit (2 lines)> u
Undid: delete lines 2-4.
edit (5 lines)> a indented line
Line 6 added.
edit (6 lines)> y
Nothing to redo.
edit (6 lines)> l 4-6
4 | eggs
5 | bread
6 | indented line
edit (6 lines)> h
a text append a line at the end
i N text insert a line before line N
r N text replace line N
d N, d N-M delete line N, or lines N to M
l, l N-M list the text, or lines N to M
f text find the lines that contain the text (any case)
u, y undo, redo
h, q this help, quit
edit (6 lines)> q
Bye.
What was typed (24 lines)
a Shopping list
a milk
a eggs
a bread
l
i 2 fruit:
r 3 2 apples
d 4
l
u
l
u
u
y
l
f MILK
d 2-4
l
u
a indented line
y
l 4-6
h
q
Modules
The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.
main.eml(entry)
eml# P029 line editor: a text of up to 30 lines, edited with short commands -
# append, insert, replace, delete, list and find - with undo and redo.
import buffer
30 => most_lines
60 => widest_line
def split_word(s):
# [the first word, the rest after the one space that follows it]; spaces
# before the word are skipped, the rest is kept as typed.
0 => i
while i < len(s) and s[i] == " ":
i + 1 => i
i => j
while j < len(s) and s[j] != " ":
j + 1 => j
if j < len(s):
return [s[i:j], s[j + 1:len(s)]]
return [s[i:j], ""]
def number(s):
# 1 to 3 digits as a number; -1 for anything else.
if s == "" or len(s) > 3:
return 0 - 1
0 => n
for c in s:
if not (c in "0123456789"):
return 0 - 1
n * 10 + int(c) => n
return n
def line_range(s):
# [first, last] for "N" or "N-M"; [] for anything else.
0 => dash
while dash < len(s) and s[dash] != "-":
dash + 1 => dash
if dash == len(s):
number(s) => n
if n == 0 - 1:
return []
return [n, n]
number(s[0:dash]) => a
number(s[dash + 1:len(s)]) => b
if a == 0 - 1 or b == 0 - 1:
return []
return [a, b]
def show(lines, n):
# Line n, numbered: " 3 | text".
str(n) => num
while len(num) < len(str(len(lines))):
" " + num => num
" " + num + " |" => out
if lines[n - 1] != "":
out + " " + lines[n - 1] => out
out ^0
def which_lines(first, last):
if first == last:
return "line " + str(first)
return "lines " + str(first) + "-" + str(last)
def out_of_range(lines):
if len(lines) == 0:
"The text is empty." ^0
elif len(lines) == 1:
"There is only line 1." ^0
else:
("Line numbers run from 1 to " + str(len(lines)) + ".") ^0
def fits(lines, text):
# Whether one more line of this text can go in; says why not.
if len(lines) >= most_lines:
("The text has " + str(most_lines) + " lines, the most it can hold.") ^0
return False
if len(text) > widest_line:
("That line has " + str(len(text)) + " characters; a line can have at most " + str(widest_line) + ".") ^0
return False
return True
def help():
" a text append a line at the end" ^0
" i N text insert a line before line N" ^0
" r N text replace line N" ^0
" d N, d N-M delete line N, or lines N to M" ^0
" l, l N-M list the text, or lines N to M" ^0
" f text find the lines that contain the text (any case)" ^0
" u, y undo, redo" ^0
" h, q this help, quit" ^0
def step(state, command):
# Carries out one command; returns the new state [lines, undo, redo].
state[0] => lines
split_word(command) => parts
buffer.lower(parts[0]) => verb
parts[1] => rest
if verb == "a" or verb == "append":
if fits(lines, rest):
buffer.edit(state, [len(lines), [], [rest], "append line " + str(len(lines) + 1)]) => state
("Line " + str(len(lines) + 1) + " added.") ^0
elif verb == "i" or verb == "insert" or verb == "r" or verb == "replace":
split_word(rest) => args
number(args[0]) => n
if n == 0 - 1:
("Type a line number and the text, such as " + verb + " 2 new text.") ^0
elif verb == "i" or verb == "insert":
if n < 1 or n > len(lines) + 1:
("A new line can go in at 1 to " + str(len(lines) + 1) + ".") ^0
elif fits(lines, args[1]):
buffer.edit(state, [n - 1, [], [args[1]], "insert line " + str(n)]) => state
("Inserted as line " + str(n) + ".") ^0
elif n < 1 or n > len(lines):
out_of_range(lines)
elif len(args[1]) > widest_line:
("That line has " + str(len(args[1])) + " characters; a line can have at most " + str(widest_line) + ".") ^0
else:
buffer.edit(state, [n - 1, [lines[n - 1]], [args[1]], "replace line " + str(n)]) => state
("Line " + str(n) + " replaced.") ^0
elif verb == "d" or verb == "delete":
line_range(rest) => r
if len(r) == 0:
"Type a line number or a range, such as d 2 or d 2-4." ^0
elif r[0] < 1 or r[1] > len(lines) or r[0] > r[1]:
out_of_range(lines)
else:
buffer.edit(state, [r[0] - 1, lines[r[0] - 1:r[1]], [], "delete " + which_lines(r[0], r[1])]) => state
("Deleted " + which_lines(r[0], r[1]) + ".") ^0
elif verb == "l" or verb == "list":
if len(lines) == 0:
"The text is empty." ^0
elif rest == "":
for n in [1:len(lines)]:
show(lines, n)
else:
line_range(rest) => r
if len(r) == 0:
"Type l, or a line number or a range, such as l 2-4." ^0
elif r[0] < 1 or r[1] > len(lines) or r[0] > r[1]:
out_of_range(lines)
else:
for n in [r[0]:r[1]]:
show(lines, n)
elif verb == "f" or verb == "find":
if rest == "":
"Type the text to find, such as f cat." ^0
else:
buffer.finds(lines, rest) => hits
if len(hits) == 0:
("No line contains '" + rest + "'.") ^0
else:
if len(hits) == 1:
("1 line contains '" + rest + "':") ^0
else:
(str(len(hits)) + " lines contain '" + rest + "':") ^0
for n in hits:
show(lines, n)
elif verb == "u" or verb == "undo":
buffer.undo(state) => result
if result[1] == None:
"Nothing to undo." ^0
else:
result[0] => state
("Undid: " + result[1][3] + ".") ^0
elif verb == "y" or verb == "redo":
buffer.redo(state) => result
if result[1] == None:
"Nothing to redo." ^0
else:
result[0] => state
("Redid: " + result[1][3] + ".") ^0
elif verb == "h" or verb == "help":
help()
elif verb != "":
("Unknown command '" + parts[0] + "'; type h for help.") ^0
return state
"== Line editor ==" ^0
("A text of up to " + str(most_lines) + " lines of at most " + str(widest_line) + " characters. Commands:") ^0
help()
[[], [], []] => state
True => running
while running:
"lines" => unit
if len(state[0]) == 1:
"line" => unit
input("edit (" + str(len(state[0])) + " " + unit + ")> ") => command
buffer.lower(split_word(command)[0]) => verb
if verb == "q" or verb == "quit":
False => running
else:
step(state, command) => state
"Bye." ^0
Python projection (main.py)
import buffer
most_lines = 30
widest_line = 60
def split_word(s):
i = 0
while i < len(s) and s[i] == " ":
i = i + 1
j = i
while j < len(s) and s[j] != " ":
j = j + 1
if j < len(s):
return [s[i:j], s[j + 1:len(s)]]
return [s[i:j], ""]
def number(s):
if s == "" or len(s) > 3:
return 0 - 1
n = 0
for c in s:
if not c in "0123456789":
return 0 - 1
n = n * 10 + int(c)
return n
def line_range(s):
dash = 0
while dash < len(s) and s[dash] != "-":
dash = dash + 1
if dash == len(s):
n = number(s)
if n == 0 - 1:
return []
return [n, n]
a = number(s[0:dash])
b = number(s[dash + 1:len(s)])
if a == 0 - 1 or b == 0 - 1:
return []
return [a, b]
def show(lines, n):
num = str(n)
while len(num) < len(str(len(lines))):
num = " " + num
out = " " + num + " |"
if lines[n - 1] != "":
out = out + " " + lines[n - 1]
print(out)
def which_lines(first, last):
if first == last:
return "line " + str(first)
return "lines " + str(first) + "-" + str(last)
def out_of_range(lines):
if len(lines) == 0:
print("The text is empty.")
elif len(lines) == 1:
print("There is only line 1.")
else:
print("Line numbers run from 1 to " + str(len(lines)) + ".")
def fits(lines, text):
if len(lines) >= most_lines:
print("The text has " + str(most_lines) + " lines, the most it can hold.")
return False
if len(text) > widest_line:
print("That line has " + str(len(text)) + " characters; a line can have at most " + str(widest_line) + ".")
return False
return True
def help():
print(" a text append a line at the end")
print(" i N text insert a line before line N")
print(" r N text replace line N")
print(" d N, d N-M delete line N, or lines N to M")
print(" l, l N-M list the text, or lines N to M")
print(" f text find the lines that contain the text (any case)")
print(" u, y undo, redo")
print(" h, q this help, quit")
def step(state, command):
lines = state[0]
parts = split_word(command)
verb = buffer.lower(parts[0])
rest = parts[1]
if verb == "a" or verb == "append":
if fits(lines, rest):
state = buffer.edit(state, [len(lines), [], [rest], "append line " + str(len(lines) + 1)])
print("Line " + str(len(lines) + 1) + " added.")
elif verb == "i" or verb == "insert" or verb == "r" or verb == "replace":
args = split_word(rest)
n = number(args[0])
if n == 0 - 1:
print("Type a line number and the text, such as " + verb + " 2 new text.")
elif verb == "i" or verb == "insert":
if n < 1 or n > len(lines) + 1:
print("A new line can go in at 1 to " + str(len(lines) + 1) + ".")
elif fits(lines, args[1]):
state = buffer.edit(state, [n - 1, [], [args[1]], "insert line " + str(n)])
print("Inserted as line " + str(n) + ".")
elif n < 1 or n > len(lines):
out_of_range(lines)
elif len(args[1]) > widest_line:
print("That line has " + str(len(args[1])) + " characters; a line can have at most " + str(widest_line) + ".")
else:
state = buffer.edit(state, [n - 1, [lines[n - 1]], [args[1]], "replace line " + str(n)])
print("Line " + str(n) + " replaced.")
elif verb == "d" or verb == "delete":
r = line_range(rest)
if len(r) == 0:
print("Type a line number or a range, such as d 2 or d 2-4.")
elif r[0] < 1 or r[1] > len(lines) or r[0] > r[1]:
out_of_range(lines)
else:
state = buffer.edit(state, [r[0] - 1, lines[r[0] - 1:r[1]], [], "delete " + which_lines(r[0], r[1])])
print("Deleted " + which_lines(r[0], r[1]) + ".")
elif verb == "l" or verb == "list":
if len(lines) == 0:
print("The text is empty.")
elif rest == "":
for n in range(1, len(lines)+1):
show(lines, n)
else:
r = line_range(rest)
if len(r) == 0:
print("Type l, or a line number or a range, such as l 2-4.")
elif r[0] < 1 or r[1] > len(lines) or r[0] > r[1]:
out_of_range(lines)
else:
for n in range(r[0], r[1]+1):
show(lines, n)
elif verb == "f" or verb == "find":
if rest == "":
print("Type the text to find, such as f cat.")
else:
hits = buffer.finds(lines, rest)
if len(hits) == 0:
print("No line contains '" + rest + "'.")
else:
if len(hits) == 1:
print("1 line contains '" + rest + "':")
else:
print(str(len(hits)) + " lines contain '" + rest + "':")
for n in hits:
show(lines, n)
elif verb == "u" or verb == "undo":
result = buffer.undo(state)
if result[1] == None:
print("Nothing to undo.")
else:
state = result[0]
print("Undid: " + result[1][3] + ".")
elif verb == "y" or verb == "redo":
result = buffer.redo(state)
if result[1] == None:
print("Nothing to redo.")
else:
state = result[0]
print("Redid: " + result[1][3] + ".")
elif verb == "h" or verb == "help":
help()
elif verb != "":
print("Unknown command '" + parts[0] + "'; type h for help.")
return state
print("== Line editor ==")
print("A text of up to " + str(most_lines) + " lines of at most " + str(widest_line) + " characters. Commands:")
help()
state = [[], [], []]
running = True
while running:
unit = "lines"
if len(state[0]) == 1:
unit = "line"
command = input("edit (" + str(len(state[0])) + " " + unit + ")> ")
verb = buffer.lower(split_word(command)[0])
if verb == "q" or verb == "quit":
running = False
else:
state = step(state, command)
print("Bye.")
buffer.eml
eml# P029 line editor - the text and its history. Every edit is one splice:
# at line index `at`, the lines `removed` come out and the lines `inserted`
# go in. A change is [at, removed, inserted, description]. Undoing a change
# is the same splice with removed and inserted swapped, so the undo and redo
# stacks only hold changes, never copies of the whole text.
def splice(lines, at, count, new_lines):
# The lines with `count` of them taken out at index `at` and new_lines
# put in their place.
return lines[0:at] + new_lines + lines[at + count:len(lines)]
def apply(lines, change):
return splice(lines, change[0], len(change[1]), change[2])
def inverse(change):
return [change[0], change[2], change[1], change[3]]
def top(stack):
return stack[len(stack) - 1]
def without_top(stack):
# The stack popped: a slice rebuild, as in the corpus case simple-stack.
return stack[0:len(stack) - 1]
def edit(state, change):
# state is [lines, undo stack, redo stack]. A new edit is applied, goes
# on the undo stack, and empties the redo stack.
return [apply(state[0], change), state[1] + [change], []]
def undo(state):
# [new state, the change undone], or [state, None] when there is nothing
# to undo.
if len(state[1]) == 0:
return [state, None]
top(state[1]) => change
return [[apply(state[0], inverse(change)), without_top(state[1]), state[2] + [change]], change]
def redo(state):
if len(state[2]) == 0:
return [state, None]
top(state[2]) => change
return [[apply(state[0], change), state[1] + [change], without_top(state[2])], change]
def finds(lines, part):
# The 1-based numbers of the lines that contain part, in any case.
lower(part) => p
[] => out
0 => i
while i < len(lines):
lower(lines[i]) => s
0 => k
False => found
while not found and k + len(p) <= len(s):
if s[k:k + len(p)] == p:
True => found
k + 1 => k
if found:
out + [i + 1] => out
i + 1 => i
return out
def lower(s):
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" => upper
"abcdefghijklmnopqrstuvwxyz" => small
"" => out
for c in s:
0 => k
while k < 26 and upper[k] != c:
k + 1 => k
if k < 26:
out + small[k] => out
else:
out + c => out
return out
Python projection (buffer.py)
def splice(lines, at, count, new_lines):
return lines[0:at] + new_lines + lines[at + count:len(lines)]
def apply(lines, change):
return splice(lines, change[0], len(change[1]), change[2])
def inverse(change):
return [change[0], change[2], change[1], change[3]]
def top(stack):
return stack[len(stack) - 1]
def without_top(stack):
return stack[0:len(stack) - 1]
def edit(state, change):
return [apply(state[0], change), state[1] + [change], []]
def undo(state):
if len(state[1]) == 0:
return [state, None]
change = top(state[1])
return [[apply(state[0], inverse(change)), without_top(state[1]), state[2] + [change]], change]
def redo(state):
if len(state[2]) == 0:
return [state, None]
change = top(state[2])
return [[apply(state[0], change), state[1] + [change], without_top(state[2])], change]
def finds(lines, part):
p = lower(part)
out = []
i = 0
while i < len(lines):
s = lower(lines[i])
k = 0
found = False
while not found and k + len(p) <= len(s):
if s[k:k + len(p)] == p:
found = True
k = k + 1
if found:
out = out + [i + 1]
i = i + 1
return out
def lower(s):
upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
small = "abcdefghijklmnopqrstuvwxyz"
out = ""
for c in s:
k = 0
while k < 26 and upper[k] != c:
k = k + 1
if k < 26:
out = out + small[k]
else:
out = out + c
return out