{
  "id": "P029",
  "slug": "line-editor",
  "key": "P029-line-editor",
  "title": "Line editor",
  "summary": "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.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P029 - Line editor\n\nA text of up to 30 lines, edited with short commands: append a line, insert\none before line N, replace line N, delete a line or a range, list all or part\nof the text, and find the lines that contain some text. Every edit can be\nundone and redone.\n\n- `main.eml` - the command line: reading a command, its checks, and what the\n  screen shows\n- `buffer.eml` - the text and its history: the splice, undo and redo, and\n  finding text\n\nHow each part works:\n\n- Every edit is one splice: at one place in the text, some lines come out and\n  some go in. Appending, inserting, replacing and deleting are all splices,\n  and each is kept as a change - where, the lines that came out, the lines\n  that went in, and a description such as `delete lines 2-4`.\n- Undo and redo are two stacks of changes. Undoing a change is the same\n  splice with the lines in and out swapped; the change then moves to the redo\n  stack, and redoing it moves it back. A new edit empties the redo stack, as\n  in most editors. The stacks hold changes, never copies of the whole text.\n  They are lists popped by a slice rebuild, as in the corpus case\n  `simple-stack`.\n- A command is its first word, in any case (`a` or `append`, and so on);\n  the text after the one space that follows the command - or the line number\n  - is kept as typed, leading spaces included, so a line can be indented.\n- Finding ignores case. Lines are listed with their numbers, right-aligned.\n\nWhat is checked: the text holds at most 30 lines of at most 60 characters; a\nnew line can go in at 1 to one past the last line; a line to replace or\ndelete, or a range (`N-M`, with N not after M), must exist. An unknown\ncommand says so and points to `h`; an empty command does nothing; `u` and\n`y` say when there is nothing to undo or redo.\n\nSessions: `sessions/basic.in` writes a four-line shopping list, inserts,\nreplaces and deletes, undoes three times and redoes once, finds `MILK`,\ndeletes a range and undoes it, appends an indented line (which empties the\nredo stack - the next `y` has nothing to redo), lists a range and shows the\nhelp; `sessions/bad-input.in` tries every command on an empty text, inserts\npast the end, gives a word for a line number, appends an empty line and a\nline of 61 characters, deletes a range that runs backwards and ranges that\nare not numbers, lists line 5 of one, searches for nothing and for text that\nis not there, fills the text to 30 lines, tries to add a 31st, replaces the\nlast line, and types a command in capitals.\n\nBuilt on the verified corpus case `simple-stack` (a list used as a stack,\npushed by adding a one-item list and popped by a slice rebuild).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P029 line editor: a text of up to 30 lines, edited with short commands -\n# append, insert, replace, delete, list and find - with undo and redo.\nimport buffer\n\n30 => most_lines\n60 => widest_line\n\ndef split_word(s):\n    # [the first word, the rest after the one space that follows it]; spaces\n    # before the word are skipped, the rest is kept as typed.\n    0 => i\n    while i < len(s) and s[i] == \" \":\n        i + 1 => i\n    i => j\n    while j < len(s) and s[j] != \" \":\n        j + 1 => j\n    if j < len(s):\n        return [s[i:j], s[j + 1:len(s)]]\n    return [s[i:j], \"\"]\n\ndef number(s):\n    # 1 to 3 digits as a number; -1 for anything else.\n    if s == \"\" or len(s) > 3:\n        return 0 - 1\n    0 => n\n    for c in s:\n        if not (c in \"0123456789\"):\n            return 0 - 1\n        n * 10 + int(c) => n\n    return n\n\ndef line_range(s):\n    # [first, last] for \"N\" or \"N-M\"; [] for anything else.\n    0 => dash\n    while dash < len(s) and s[dash] != \"-\":\n        dash + 1 => dash\n    if dash == len(s):\n        number(s) => n\n        if n == 0 - 1:\n            return []\n        return [n, n]\n    number(s[0:dash]) => a\n    number(s[dash + 1:len(s)]) => b\n    if a == 0 - 1 or b == 0 - 1:\n        return []\n    return [a, b]\n\ndef show(lines, n):\n    # Line n, numbered: \"  3 | text\".\n    str(n) => num\n    while len(num) < len(str(len(lines))):\n        \" \" + num => num\n    \"  \" + num + \" |\" => out\n    if lines[n - 1] != \"\":\n        out + \" \" + lines[n - 1] => out\n    out ^0\n\ndef which_lines(first, last):\n    if first == last:\n        return \"line \" + str(first)\n    return \"lines \" + str(first) + \"-\" + str(last)\n\ndef out_of_range(lines):\n    if len(lines) == 0:\n        \"The text is empty.\" ^0\n    elif len(lines) == 1:\n        \"There is only line 1.\" ^0\n    else:\n        (\"Line numbers run from 1 to \" + str(len(lines)) + \".\") ^0\n\ndef fits(lines, text):\n    # Whether one more line of this text can go in; says why not.\n    if len(lines) >= most_lines:\n        (\"The text has \" + str(most_lines) + \" lines, the most it can hold.\") ^0\n        return False\n    if len(text) > widest_line:\n        (\"That line has \" + str(len(text)) + \" characters; a line can have at most \" + str(widest_line) + \".\") ^0\n        return False\n    return True\n\ndef help():\n    \"  a text       append a line at the end\" ^0\n    \"  i N text     insert a line before line N\" ^0\n    \"  r N text     replace line N\" ^0\n    \"  d N, d N-M   delete line N, or lines N to M\" ^0\n    \"  l, l N-M     list the text, or lines N to M\" ^0\n    \"  f text       find the lines that contain the text (any case)\" ^0\n    \"  u, y         undo, redo\" ^0\n    \"  h, q         this help, quit\" ^0\n\ndef step(state, command):\n    # Carries out one command; returns the new state [lines, undo, redo].\n    state[0] => lines\n    split_word(command) => parts\n    buffer.lower(parts[0]) => verb\n    parts[1] => rest\n    if verb == \"a\" or verb == \"append\":\n        if fits(lines, rest):\n            buffer.edit(state, [len(lines), [], [rest], \"append line \" + str(len(lines) + 1)]) => state\n            (\"Line \" + str(len(lines) + 1) + \" added.\") ^0\n    elif verb == \"i\" or verb == \"insert\" or verb == \"r\" or verb == \"replace\":\n        split_word(rest) => args\n        number(args[0]) => n\n        if n == 0 - 1:\n            (\"Type a line number and the text, such as \" + verb + \" 2 new text.\") ^0\n        elif verb == \"i\" or verb == \"insert\":\n            if n < 1 or n > len(lines) + 1:\n                (\"A new line can go in at 1 to \" + str(len(lines) + 1) + \".\") ^0\n            elif fits(lines, args[1]):\n                buffer.edit(state, [n - 1, [], [args[1]], \"insert line \" + str(n)]) => state\n                (\"Inserted as line \" + str(n) + \".\") ^0\n        elif n < 1 or n > len(lines):\n            out_of_range(lines)\n        elif len(args[1]) > widest_line:\n            (\"That line has \" + str(len(args[1])) + \" characters; a line can have at most \" + str(widest_line) + \".\") ^0\n        else:\n            buffer.edit(state, [n - 1, [lines[n - 1]], [args[1]], \"replace line \" + str(n)]) => state\n            (\"Line \" + str(n) + \" replaced.\") ^0\n    elif verb == \"d\" or verb == \"delete\":\n        line_range(rest) => r\n        if len(r) == 0:\n            \"Type a line number or a range, such as d 2 or d 2-4.\" ^0\n        elif r[0] < 1 or r[1] > len(lines) or r[0] > r[1]:\n            out_of_range(lines)\n        else:\n            buffer.edit(state, [r[0] - 1, lines[r[0] - 1:r[1]], [], \"delete \" + which_lines(r[0], r[1])]) => state\n            (\"Deleted \" + which_lines(r[0], r[1]) + \".\") ^0\n    elif verb == \"l\" or verb == \"list\":\n        if len(lines) == 0:\n            \"The text is empty.\" ^0\n        elif rest == \"\":\n            for n in [1:len(lines)]:\n                show(lines, n)\n        else:\n            line_range(rest) => r\n            if len(r) == 0:\n                \"Type l, or a line number or a range, such as l 2-4.\" ^0\n            elif r[0] < 1 or r[1] > len(lines) or r[0] > r[1]:\n                out_of_range(lines)\n            else:\n                for n in [r[0]:r[1]]:\n                    show(lines, n)\n    elif verb == \"f\" or verb == \"find\":\n        if rest == \"\":\n            \"Type the text to find, such as f cat.\" ^0\n        else:\n            buffer.finds(lines, rest) => hits\n            if len(hits) == 0:\n                (\"No line contains '\" + rest + \"'.\") ^0\n            else:\n                if len(hits) == 1:\n                    (\"1 line contains '\" + rest + \"':\") ^0\n                else:\n                    (str(len(hits)) + \" lines contain '\" + rest + \"':\") ^0\n                for n in hits:\n                    show(lines, n)\n    elif verb == \"u\" or verb == \"undo\":\n        buffer.undo(state) => result\n        if result[1] == None:\n            \"Nothing to undo.\" ^0\n        else:\n            result[0] => state\n            (\"Undid: \" + result[1][3] + \".\") ^0\n    elif verb == \"y\" or verb == \"redo\":\n        buffer.redo(state) => result\n        if result[1] == None:\n            \"Nothing to redo.\" ^0\n        else:\n            result[0] => state\n            (\"Redid: \" + result[1][3] + \".\") ^0\n    elif verb == \"h\" or verb == \"help\":\n        help()\n    elif verb != \"\":\n        (\"Unknown command '\" + parts[0] + \"'; type h for help.\") ^0\n    return state\n\n\"== Line editor ==\" ^0\n(\"A text of up to \" + str(most_lines) + \" lines of at most \" + str(widest_line) + \" characters. Commands:\") ^0\nhelp()\n[[], [], []] => state\nTrue => running\nwhile running:\n    \"lines\" => unit\n    if len(state[0]) == 1:\n        \"line\" => unit\n    input(\"edit (\" + str(len(state[0])) + \" \" + unit + \")> \") => command\n    buffer.lower(split_word(command)[0]) => verb\n    if verb == \"q\" or verb == \"quit\":\n        False => running\n    else:\n        step(state, command) => state\n\"Bye.\" ^0\n",
      "python": "import buffer\nmost_lines = 30\nwidest_line = 60\n\ndef split_word(s):\n    i = 0\n    while i < len(s) and s[i] == \" \":\n        i = i + 1\n    j = i\n    while j < len(s) and s[j] != \" \":\n        j = j + 1\n    if j < len(s):\n        return [s[i:j], s[j + 1:len(s)]]\n    return [s[i:j], \"\"]\n\ndef number(s):\n    if s == \"\" or len(s) > 3:\n        return 0 - 1\n    n = 0\n    for c in s:\n        if not c in \"0123456789\":\n            return 0 - 1\n        n = n * 10 + int(c)\n    return n\n\ndef line_range(s):\n    dash = 0\n    while dash < len(s) and s[dash] != \"-\":\n        dash = dash + 1\n    if dash == len(s):\n        n = number(s)\n        if n == 0 - 1:\n            return []\n        return [n, n]\n    a = number(s[0:dash])\n    b = number(s[dash + 1:len(s)])\n    if a == 0 - 1 or b == 0 - 1:\n        return []\n    return [a, b]\n\ndef show(lines, n):\n    num = str(n)\n    while len(num) < len(str(len(lines))):\n        num = \" \" + num\n    out = \"  \" + num + \" |\"\n    if lines[n - 1] != \"\":\n        out = out + \" \" + lines[n - 1]\n    print(out)\n\ndef which_lines(first, last):\n    if first == last:\n        return \"line \" + str(first)\n    return \"lines \" + str(first) + \"-\" + str(last)\n\ndef out_of_range(lines):\n    if len(lines) == 0:\n        print(\"The text is empty.\")\n    elif len(lines) == 1:\n        print(\"There is only line 1.\")\n    else:\n        print(\"Line numbers run from 1 to \" + str(len(lines)) + \".\")\n\ndef fits(lines, text):\n    if len(lines) >= most_lines:\n        print(\"The text has \" + str(most_lines) + \" lines, the most it can hold.\")\n        return False\n    if len(text) > widest_line:\n        print(\"That line has \" + str(len(text)) + \" characters; a line can have at most \" + str(widest_line) + \".\")\n        return False\n    return True\n\ndef help():\n    print(\"  a text       append a line at the end\")\n    print(\"  i N text     insert a line before line N\")\n    print(\"  r N text     replace line N\")\n    print(\"  d N, d N-M   delete line N, or lines N to M\")\n    print(\"  l, l N-M     list the text, or lines N to M\")\n    print(\"  f text       find the lines that contain the text (any case)\")\n    print(\"  u, y         undo, redo\")\n    print(\"  h, q         this help, quit\")\n\ndef step(state, command):\n    lines = state[0]\n    parts = split_word(command)\n    verb = buffer.lower(parts[0])\n    rest = parts[1]\n    if verb == \"a\" or verb == \"append\":\n        if fits(lines, rest):\n            state = buffer.edit(state, [len(lines), [], [rest], \"append line \" + str(len(lines) + 1)])\n            print(\"Line \" + str(len(lines) + 1) + \" added.\")\n    elif verb == \"i\" or verb == \"insert\" or verb == \"r\" or verb == \"replace\":\n        args = split_word(rest)\n        n = number(args[0])\n        if n == 0 - 1:\n            print(\"Type a line number and the text, such as \" + verb + \" 2 new text.\")\n        elif verb == \"i\" or verb == \"insert\":\n            if n < 1 or n > len(lines) + 1:\n                print(\"A new line can go in at 1 to \" + str(len(lines) + 1) + \".\")\n            elif fits(lines, args[1]):\n                state = buffer.edit(state, [n - 1, [], [args[1]], \"insert line \" + str(n)])\n                print(\"Inserted as line \" + str(n) + \".\")\n        elif n < 1 or n > len(lines):\n            out_of_range(lines)\n        elif len(args[1]) > widest_line:\n            print(\"That line has \" + str(len(args[1])) + \" characters; a line can have at most \" + str(widest_line) + \".\")\n        else:\n            state = buffer.edit(state, [n - 1, [lines[n - 1]], [args[1]], \"replace line \" + str(n)])\n            print(\"Line \" + str(n) + \" replaced.\")\n    elif verb == \"d\" or verb == \"delete\":\n        r = line_range(rest)\n        if len(r) == 0:\n            print(\"Type a line number or a range, such as d 2 or d 2-4.\")\n        elif r[0] < 1 or r[1] > len(lines) or r[0] > r[1]:\n            out_of_range(lines)\n        else:\n            state = buffer.edit(state, [r[0] - 1, lines[r[0] - 1:r[1]], [], \"delete \" + which_lines(r[0], r[1])])\n            print(\"Deleted \" + which_lines(r[0], r[1]) + \".\")\n    elif verb == \"l\" or verb == \"list\":\n        if len(lines) == 0:\n            print(\"The text is empty.\")\n        elif rest == \"\":\n            for n in range(1, len(lines)+1):\n                show(lines, n)\n        else:\n            r = line_range(rest)\n            if len(r) == 0:\n                print(\"Type l, or a line number or a range, such as l 2-4.\")\n            elif r[0] < 1 or r[1] > len(lines) or r[0] > r[1]:\n                out_of_range(lines)\n            else:\n                for n in range(r[0], r[1]+1):\n                    show(lines, n)\n    elif verb == \"f\" or verb == \"find\":\n        if rest == \"\":\n            print(\"Type the text to find, such as f cat.\")\n        else:\n            hits = buffer.finds(lines, rest)\n            if len(hits) == 0:\n                print(\"No line contains '\" + rest + \"'.\")\n            else:\n                if len(hits) == 1:\n                    print(\"1 line contains '\" + rest + \"':\")\n                else:\n                    print(str(len(hits)) + \" lines contain '\" + rest + \"':\")\n                for n in hits:\n                    show(lines, n)\n    elif verb == \"u\" or verb == \"undo\":\n        result = buffer.undo(state)\n        if result[1] == None:\n            print(\"Nothing to undo.\")\n        else:\n            state = result[0]\n            print(\"Undid: \" + result[1][3] + \".\")\n    elif verb == \"y\" or verb == \"redo\":\n        result = buffer.redo(state)\n        if result[1] == None:\n            print(\"Nothing to redo.\")\n        else:\n            state = result[0]\n            print(\"Redid: \" + result[1][3] + \".\")\n    elif verb == \"h\" or verb == \"help\":\n        help()\n    elif verb != \"\":\n        print(\"Unknown command '\" + parts[0] + \"'; type h for help.\")\n    return state\n\nprint(\"== Line editor ==\")\nprint(\"A text of up to \" + str(most_lines) + \" lines of at most \" + str(widest_line) + \" characters. Commands:\")\nhelp()\nstate = [[], [], []]\nrunning = True\nwhile running:\n    unit = \"lines\"\n    if len(state[0]) == 1:\n        unit = \"line\"\n    command = input(\"edit (\" + str(len(state[0])) + \" \" + unit + \")> \")\n    verb = buffer.lower(split_word(command)[0])\n    if verb == \"q\" or verb == \"quit\":\n        running = False\n    else:\n        state = step(state, command)\nprint(\"Bye.\")\n"
    },
    {
      "name": "buffer.eml",
      "eml": "# P029 line editor - the text and its history. Every edit is one splice:\n# at line index `at`, the lines `removed` come out and the lines `inserted`\n# go in. A change is [at, removed, inserted, description]. Undoing a change\n# is the same splice with removed and inserted swapped, so the undo and redo\n# stacks only hold changes, never copies of the whole text.\n\ndef splice(lines, at, count, new_lines):\n    # The lines with `count` of them taken out at index `at` and new_lines\n    # put in their place.\n    return lines[0:at] + new_lines + lines[at + count:len(lines)]\n\ndef apply(lines, change):\n    return splice(lines, change[0], len(change[1]), change[2])\n\ndef inverse(change):\n    return [change[0], change[2], change[1], change[3]]\n\ndef top(stack):\n    return stack[len(stack) - 1]\n\ndef without_top(stack):\n    # The stack popped: a slice rebuild, as in the corpus case simple-stack.\n    return stack[0:len(stack) - 1]\n\ndef edit(state, change):\n    # state is [lines, undo stack, redo stack]. A new edit is applied, goes\n    # on the undo stack, and empties the redo stack.\n    return [apply(state[0], change), state[1] + [change], []]\n\ndef undo(state):\n    # [new state, the change undone], or [state, None] when there is nothing\n    # to undo.\n    if len(state[1]) == 0:\n        return [state, None]\n    top(state[1]) => change\n    return [[apply(state[0], inverse(change)), without_top(state[1]), state[2] + [change]], change]\n\ndef redo(state):\n    if len(state[2]) == 0:\n        return [state, None]\n    top(state[2]) => change\n    return [[apply(state[0], change), state[1] + [change], without_top(state[2])], change]\n\ndef finds(lines, part):\n    # The 1-based numbers of the lines that contain part, in any case.\n    lower(part) => p\n    [] => out\n    0 => i\n    while i < len(lines):\n        lower(lines[i]) => s\n        0 => k\n        False => found\n        while not found and k + len(p) <= len(s):\n            if s[k:k + len(p)] == p:\n                True => found\n            k + 1 => k\n        if found:\n            out + [i + 1] => out\n        i + 1 => i\n    return out\n\ndef lower(s):\n    \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\" => upper\n    \"abcdefghijklmnopqrstuvwxyz\" => small\n    \"\" => out\n    for c in s:\n        0 => k\n        while k < 26 and upper[k] != c:\n            k + 1 => k\n        if k < 26:\n            out + small[k] => out\n        else:\n            out + c => out\n    return out\n",
      "python": "def splice(lines, at, count, new_lines):\n    return lines[0:at] + new_lines + lines[at + count:len(lines)]\n\ndef apply(lines, change):\n    return splice(lines, change[0], len(change[1]), change[2])\n\ndef inverse(change):\n    return [change[0], change[2], change[1], change[3]]\n\ndef top(stack):\n    return stack[len(stack) - 1]\n\ndef without_top(stack):\n    return stack[0:len(stack) - 1]\n\ndef edit(state, change):\n    return [apply(state[0], change), state[1] + [change], []]\n\ndef undo(state):\n    if len(state[1]) == 0:\n        return [state, None]\n    change = top(state[1])\n    return [[apply(state[0], inverse(change)), without_top(state[1]), state[2] + [change]], change]\n\ndef redo(state):\n    if len(state[2]) == 0:\n        return [state, None]\n    change = top(state[2])\n    return [[apply(state[0], change), state[1] + [change], without_top(state[2])], change]\n\ndef finds(lines, part):\n    p = lower(part)\n    out = []\n    i = 0\n    while i < len(lines):\n        s = lower(lines[i])\n        k = 0\n        found = False\n        while not found and k + len(p) <= len(s):\n            if s[k:k + len(p)] == p:\n                found = True\n            k = k + 1\n        if found:\n            out = out + [i + 1]\n        i = i + 1\n    return out\n\ndef lower(s):\n    upper = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n    small = \"abcdefghijklmnopqrstuvwxyz\"\n    out = \"\"\n    for c in s:\n        k = 0\n        while k < 26 and upper[k] != c:\n            k = k + 1\n        if k < 26:\n            out = out + small[k]\n        else:\n            out = out + c\n    return out\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "\nx\nl\nu\ny\nd 1\nr 1 new\ni 2 first\ni one two\na\nl\na zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz\nd 2-1\nd x\nd 1-x\nl 5\nl a-b\nf\nf zzz\na line 2\na line 3\na line 4\na line 5\na line 6\na line 7\na line 8\na line 9\na line 10\na line 11\na line 12\na line 13\na line 14\na line 15\na line 16\na line 17\na line 18\na line 19\na line 20\na line 21\na line 22\na line 23\na line 24\na line 25\na line 26\na line 27\na line 28\na line 29\na line 30\na one more\ni 1 also no\nr 30 the last line\nl 29-30\nAPPEND\nQ\n",
      "screen": "== Line editor ==\nA text of up to 30 lines of at most 60 characters. Commands:\n  a text       append a line at the end\n  i N text     insert a line before line N\n  r N text     replace line N\n  d N, d N-M   delete line N, or lines N to M\n  l, l N-M     list the text, or lines N to M\n  f text       find the lines that contain the text (any case)\n  u, y         undo, redo\n  h, q         this help, quit\nedit (0 lines)> \nedit (0 lines)> x\nUnknown command 'x'; type h for help.\nedit (0 lines)> l\nThe text is empty.\nedit (0 lines)> u\nNothing to undo.\nedit (0 lines)> y\nNothing to redo.\nedit (0 lines)> d 1\nThe text is empty.\nedit (0 lines)> r 1 new\nThe text is empty.\nedit (0 lines)> i 2 first\nA new line can go in at 1 to 1.\nedit (0 lines)> i one two\nType a line number and the text, such as i 2 new text.\nedit (0 lines)> a\nLine 1 added.\nedit (1 line)> l\n  1 |\nedit (1 line)> a zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz\nThat line has 61 characters; a line can have at most 60.\nedit (1 line)> d 2-1\nThere is only line 1.\nedit (1 line)> d x\nType a line number or a range, such as d 2 or d 2-4.\nedit (1 line)> d 1-x\nType a line number or a range, such as d 2 or d 2-4.\nedit (1 line)> l 5\nThere is only line 1.\nedit (1 line)> l a-b\nType l, or a line number or a range, such as l 2-4.\nedit (1 line)> f\nType the text to find, such as f cat.\nedit (1 line)> f zzz\nNo line contains 'zzz'.\nedit (1 line)> a line 2\nLine 2 added.\nedit (2 lines)> a line 3\nLine 3 added.\nedit (3 lines)> a line 4\nLine 4 added.\nedit (4 lines)> a line 5\nLine 5 added.\nedit (5 lines)> a line 6\nLine 6 added.\nedit (6 lines)> a line 7\nLine 7 added.\nedit (7 lines)> a line 8\nLine 8 added.\nedit (8 lines)> a line 9\nLine 9 added.\nedit (9 lines)> a line 10\nLine 10 added.\nedit (10 lines)> a line 11\nLine 11 added.\nedit (11 lines)> a line 12\nLine 12 added.\nedit (12 lines)> a line 13\nLine 13 added.\nedit (13 lines)> a line 14\nLine 14 added.\nedit (14 lines)> a line 15\nLine 15 added.\nedit (15 lines)> a line 16\nLine 16 added.\nedit (16 lines)> a line 17\nLine 17 added.\nedit (17 lines)> a line 18\nLine 18 added.\nedit (18 lines)> a line 19\nLine 19 added.\nedit (19 lines)> a line 20\nLine 20 added.\nedit (20 lines)> a line 21\nLine 21 added.\nedit (21 lines)> a line 22\nLine 22 added.\nedit (22 lines)> a line 23\nLine 23 added.\nedit (23 lines)> a line 24\nLine 24 added.\nedit (24 lines)> a line 25\nLine 25 added.\nedit (25 lines)> a line 26\nLine 26 added.\nedit (26 lines)> a line 27\nLine 27 added.\nedit (27 lines)> a line 28\nLine 28 added.\nedit (28 lines)> a line 29\nLine 29 added.\nedit (29 lines)> a line 30\nLine 30 added.\nedit (30 lines)> a one more\nThe text has 30 lines, the most it can hold.\nedit (30 lines)> i 1 also no\nThe text has 30 lines, the most it can hold.\nedit (30 lines)> r 30 the last line\nLine 30 replaced.\nedit (30 lines)> l 29-30\n  29 | line 29\n  30 | the last line\nedit (30 lines)> APPEND\nThe text has 30 lines, the most it can hold.\nedit (30 lines)> Q\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "a Shopping list\na milk\na eggs\na bread\nl\ni 2 fruit:\nr 3 2 apples\nd 4\nl\nu\nl\nu\nu\ny\nl\nf MILK\nd 2-4\nl\nu\na   indented line\ny\nl 4-6\nh\nq\n",
      "screen": "== Line editor ==\nA text of up to 30 lines of at most 60 characters. Commands:\n  a text       append a line at the end\n  i N text     insert a line before line N\n  r N text     replace line N\n  d N, d N-M   delete line N, or lines N to M\n  l, l N-M     list the text, or lines N to M\n  f text       find the lines that contain the text (any case)\n  u, y         undo, redo\n  h, q         this help, quit\nedit (0 lines)> a Shopping list\nLine 1 added.\nedit (1 line)> a milk\nLine 2 added.\nedit (2 lines)> a eggs\nLine 3 added.\nedit (3 lines)> a bread\nLine 4 added.\nedit (4 lines)> l\n  1 | Shopping list\n  2 | milk\n  3 | eggs\n  4 | bread\nedit (4 lines)> i 2 fruit:\nInserted as line 2.\nedit (5 lines)> r 3 2 apples\nLine 3 replaced.\nedit (5 lines)> d 4\nDeleted line 4.\nedit (4 lines)> l\n  1 | Shopping list\n  2 | fruit:\n  3 | 2 apples\n  4 | bread\nedit (4 lines)> u\nUndid: delete line 4.\nedit (5 lines)> l\n  1 | Shopping list\n  2 | fruit:\n  3 | 2 apples\n  4 | eggs\n  5 | bread\nedit (5 lines)> u\nUndid: replace line 3.\nedit (5 lines)> u\nUndid: insert line 2.\nedit (4 lines)> y\nRedid: insert line 2.\nedit (5 lines)> l\n  1 | Shopping list\n  2 | fruit:\n  3 | milk\n  4 | eggs\n  5 | bread\nedit (5 lines)> f MILK\n1 line contains 'MILK':\n  3 | milk\nedit (5 lines)> d 2-4\nDeleted lines 2-4.\nedit (2 lines)> l\n  1 | Shopping list\n  2 | bread\nedit (2 lines)> u\nUndid: delete lines 2-4.\nedit (5 lines)> a   indented line\nLine 6 added.\nedit (6 lines)> y\nNothing to redo.\nedit (6 lines)> l 4-6\n  4 | eggs\n  5 | bread\n  6 |   indented line\nedit (6 lines)> h\n  a text       append a line at the end\n  i N text     insert a line before line N\n  r N text     replace line N\n  d N, d N-M   delete line N, or lines N to M\n  l, l N-M     list the text, or lines N to M\n  f text       find the lines that contain the text (any case)\n  u, y         undo, redo\n  h, q         this help, quit\nedit (6 lines)> q\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "simple-stack",
      "caseId": "042-simple-stack",
      "title": "Case corpus: a self-authored LIFO stack"
    }
  ],
  "updated": "2026-10-06"
}
