Tab finishes a word somebody started

The first word of a line, against the shell's fifteen commands. One match goes in
with a space after it, because a word that can only be one thing is finished.
Several are folded into their longest common prefix and that goes in, which is
the most that can be said without guessing which was meant - and if that adds
nothing, the matches are listed and the line put back underneath.

THE LINE COMING BACK IS THE HALF I EXPECTED TO BE HARD and it was already
solved. The prompt has been reprinted somewhere else entirely, so the editor's
idea of where the line begins is wrong - but editAnchor works that out backwards
from where printing ended, precisely so it survives the screen moving. Listing is
a redraw it already knew how to do.

editInsert became editPut, a routine, because completing a word puts in several
characters and every one of them is that. Which cost a bug immediately: the old
inline code left the insertion point in A, and a RET puts A back to what the
caller had.

Two more bugs worth naming, both mine and both the same shape - a pointer that
had moved:

THE CANDIDATE'S START HAS TO BE KEPT. The comparison walks DP3 through the name
as it matches, so by the time a match is declared, DP3 points at the part AFTER
what was typed - and that is what got copied. "he" completed to "he" because the
answer taken was "lp".

AND THE INSERTION STOPS AT OR PAST, not exactly equal. With the wrong answer the
two counters passed each other and the loop ran off the end of the buffer,
filling the line with whatever was next in memory. They cannot pass each other
now, and the branch stays, because the cheaper failure is worth nothing.

MY OWN TEST HAD A HOLE and breaking the code found it. The later-word case
pressed Tab after a space, where there is nothing to finish anyway, so it passed
whether or not the shell checked which word it was on. It types "echo he" now,
which would become "echo help" if it did not.

The assembler's label table went past 1024 and is doubled. A ceiling reached
once will be reached again, and it is pointers into source already in memory.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-09-01 17:43:48 -04:00
co-authored by Claude Opus 5
parent 749fef8ce2
commit bb065fe221
7 changed files with 615 additions and 45 deletions
+21
View File
@@ -299,6 +299,7 @@ line can be moved about in at all.
| Backspace | Take out the character before the cursor. |
| Delete | Take out the one under it. |
| Up, Down | Walk back through the last eight lines, and forward again. |
| Tab | Finish the word being typed, if there is only one thing it could be. |
| Return | Finish the line, wherever the cursor happens to be sitting. |
Anything typed goes in where the cursor is, so a word left out of the middle of a line is
@@ -312,6 +313,26 @@ line, and nowhere for a history to live. Now the console delivers keys and says
about what they mean, the same way it reports what a drive is and says nothing about what
should be on it, and the shell decides.
### Finishing A Word:
Tab finishes the **first** word of a line against the shell's own commands. One match goes in
with a space after it, because a word that can only be one thing is finished. Several are
folded into their longest common prefix and that goes in, which is the most that can be said
without guessing which was meant - and if that adds nothing, because what is typed is already
as far as they all agree, the matches are listed and the line put back underneath.
Nothing typed and nothing matching both do nothing, quietly. Finishing an argument rather
than a command is not built yet.
**This is the same wall the history was behind.** A shell that never sees a keystroke has no
moment at which somebody has typed half a word - the terminal hands over finished lines - so
there is nothing to press Tab at. What made it possible was moving the line editing into the
system, and everything since has been downstream of that one decision.
The commands are walkable because they are **packed**: fifteen names, each ending in the zero
that says where the next begins. The dispatch is a chain of comparisons and cannot be walked,
so the names have to be data as well as code, and `make test` checks that the two agree.
### The History:
The last eight lines are kept, and Up walks back through them. It is a ring: a ninth line
+531 -43
View File
@@ -593,6 +593,9 @@ editNotEnd:
INIB 0x81
XOR
BRQ editDown
INIB 0x09
XOR
BRQ tabComplete
; Anything else is a character if it is printable and nothing at all if it is not. The
; console's own keys that this shell has no use for land here and are ignored rather than
@@ -608,52 +611,14 @@ editNotEnd:
; ---- Putting a character in ----
editInsert:
SETD.1 EditLength
LDA.1
SETD.1 EditRoom
LDB.1
CCF
SUB
BNC editKey ; Full. The zero byte on the end is not counted in the room.
CALL editPut
BNQ editKey ; No room for it. The zero byte on the end is not counted.
; A hole at the insertion point, made from the top down so nothing is overwritten before
; it has been moved.
SETD.1 EditLength
LDA.1
SETD.1 EditAt
LDB.1
CCF
SUB
MVQB ; How many characters are above the insertion point.
SETD.1 EditBase
LDD.0.1
SETD.1 EditLength
LDA.1
DPUA.0 ; One past the last character.
editInsertShift:
BRB editInsertPut
DECD.0
LDA.0
INCD.0
STA.0
DECD.0
DECB
BRI editInsertShift
editInsertPut:
SETD.1 EditChar
LDA.1
STA.0
SETD.1 EditLength
LDA.1
INCA
STA.1
; At the end of the line, printing it IS the change. The insertion point is READ BACK
; rather than still being in A: it was, while this was one run of code, and a RET puts A
; back to whatever the caller had.
SETD.1 EditAt
LDA.1
INCA
STA.1
; At the end of the line, printing it IS the change.
SETD.1 EditLength
LDB.1
CCF
@@ -671,6 +636,70 @@ editInsertRedraw:
CALL editRedraw
BRI editKey
; Puts the character in EditChar into the line where the insertion point is, and moves the
; insertion point past it. DRAWS NOTHING: what that should look like depends on where in the
; line it went, and the caller is what knows. Q is zero if it went in and one if there was
; no room.
;
; A routine rather than the run of code it used to be, because Tab completing a word puts in
; several characters and every one of them is this.
editPut:
SETD.1 EditLength
LDA.1
SETD.1 EditRoom
LDB.1
CCF
SUB
BNC editPutFull
; A hole at the insertion point, made from the top down so nothing is overwritten before
; it has been moved.
SETD.1 EditLength
LDA.1
SETD.1 EditAt
LDB.1
CCF
SUB
MVQB ; How many characters are above the insertion point.
SETD.1 EditBase
LDD.0.1
SETD.1 EditLength
LDA.1
DPUA.0 ; One past the last character.
editPutShift:
BRB editPutHere
DECD.0
LDA.0
INCD.0
STA.0
DECD.0
DECB
BRI editPutShift
editPutHere:
SETD.1 EditChar
LDA.1
STA.0
SETD.1 EditLength
LDA.1
INCA
STA.1
SETD.1 EditAt
LDA.1
INCA
STA.1
RSTA
RSTB
CCF
ADD ; Q is zero: it went in.
RET
editPutFull:
INIA 0x01
RSTB
CCF
ADD ; Q is one: it did not.
RET
; ---- Taking one out ----
;
; Backspace and Delete are different keys doing different things: one takes the character
@@ -1478,6 +1507,433 @@ faultScreen:
OUTA 0x02
RET
; ---- Finishing a word somebody started ----
;
; Tab. What it can finish depends on where in the line it is: the FIRST word is a command or
; the name of a program, and everything after it is a file. Only the first is done here.
;
; None of this could have existed a fortnight ago. The shell never saw a keystroke - the
; terminal handed it a finished line - so there was no moment at which somebody had typed
; half a word and the system knew about it. That is the same wall the history was behind.
;
; ---- What it does with what it finds ----
;
; One match goes in with a space after it, because a word that can only be one thing is
; finished. Several are folded into their longest common prefix and that goes in, which is
; the most that can be said without guessing. If that adds nothing - because what is typed
; IS already the common prefix - the matches are listed and the line put back underneath.
;
; EVERY HELPER BELOW ANSWERS IN Q. A RET puts A, B and the first three pointers back, so a
; routine here that answered in A would be answering into a register its caller cannot see.
tabComplete:
; ---- Which word, and is it the first ----
;
; Back from the insertion point to a space or the start of the line. What lies between
; there and the cursor is what somebody has typed, and what lies before it decides whether
; this is a command being named or an argument being given.
SETD.1 EditAt
LDA.1
SETD.1 TabWordAt
STA.1
tabBack:
SETD.1 TabWordAt
LDA.1
BRA tabIsFirst ; The start of the line is the start of the word, and of the line.
DECA
CALL tabCharAt
MVQA
INIB 0x20
XOR
BRQ tabFoundStart ; A space, so the word begins after it.
SETD.1 TabWordAt
LDA.1
DECA
STA.1
BRI tabBack
tabFoundStart:
; There is a space behind the word, so something may be in front of that space. Only a run
; of spaces means this is still the first word; anything else is an argument, and finishing
; one of those is not built yet.
SETD.1 TabWordAt
LDA.1
SETD.1 TabLeft
STA.1
tabFirstCheck:
SETD.1 TabLeft
LDA.1
BRA tabIsFirst
DECA
STA.1
CALL tabCharAt
MVQA
INIB 0x20
XOR
BNQ tabDone ; Not a space, so a word came before this one.
BRI tabFirstCheck
tabIsFirst:
; How much of it is typed. Nothing at all is not a question anybody asked - it would offer
; every command there is, which is what help is for.
SETD.1 EditAt
LDA.1
SETD.1 TabWordAt
LDB.1
CCF
SUB
BRQ tabDone
MVQA
SETD.1 TabWordLen
STA.1
RSTA
SETD.1 TabCount
STA.1
SETD.1 TabBestLen
STA.1
CALL tabTryNames
SETD.1 TabCount
LDA.1
BRA tabDone ; Nothing matched, and saying so would be noise.
; ---- Putting it in ----
;
; Everything of the answer past what was already typed. The insertion point is at the end
; of the word, so each character goes in where the one before it left off.
SETD.1 TabWordLen
LDA.1
SETD.1 TabPut
STA.1
tabPutNext:
SETD.1 TabPut
LDA.1
SETD.1 TabBestLen
LDB.1
CCF
SUB
; At or past, rather than exactly equal. They cannot pass each other now - a candidate is
; only offered if it matches what was typed, so the answer is never shorter than that - but
; they DID while the answer was being taken from the wrong place, and the difference
; between the two branches was a line that ran off the end and a line that simply did not
; grow. The cheaper failure is worth a branch that costs nothing.
BNC tabPutDone
SETD.3 TabBest
SETD.1 TabPut
LDA.1
DPUA.3
LDA.3
SETD.1 EditChar
STA.1
CALL editPut
BNQ tabPutDone ; The line is full, so the rest of the answer will not fit.
SETD.1 TabPut
LDA.1
INCA
STA.1
BRI tabPutNext
tabPutDone:
SETD.1 TabCount
LDA.1
INIB 0d1
XOR
BNQ tabAmbiguous
; Only one thing it can be, so it is finished and a space says so.
INIA 0x20
SETD.1 EditChar
STA.1
CALL editPut
CALL editRedraw
BRI tabDone
tabAmbiguous:
; Several. The common prefix has gone in; if it added nothing then what was typed is
; already as far as they all agree, and the only useful thing left is to show them.
SETD.1 TabBestLen
LDA.1
SETD.1 TabWordLen
LDB.1
CCF
SUB
BRQ tabList
CALL editRedraw
tabDone:
BRI editKey
; A holds an offset into the line. Q is the character there.
tabCharAt:
SETD.1 EditBase
LDD.0.1
DPUA.0
LDA.0
RSTB
CCF
ADD
RET
; ---- Offering a candidate ----
;
; DP3 names a string ending in a zero. If it begins with what has been typed it is folded
; into the answer, and DP3 is left just past that zero either way - which is what makes a run
; of packed strings walkable by calling this over and over.
tabOffer:
; Where this candidate begins, before the comparison below walks DP3 through it. What is
; taken as the answer is the WHOLE name and not the part after the bit that matched.
SETD.1 TabStart
STD.3.1
SETD.1 TabWordLen
LDA.1
SETD.1 TabSeen
STA.1
RSTA
SETD.1 TabAt
STA.1
tabOfferMatch:
SETD.1 TabSeen
LDA.1
BRA tabOfferTake ; Every typed character agreed.
DECA
STA.1
LDA.3
BRA tabOfferSkip ; The candidate ended before what was typed did.
SETD.1 TabHold
STA.1 ; Kept, while the line's character is fetched.
SETD.1 TabWordAt
LDA.1
SETD.1 TabAt
LDB.1
CCF
ADD
MVQA
CALL tabCharAt
MVQA
SETD.1 TabHold
LDB.1
XOR
BNQ tabOfferSkip
INCD.3
SETD.1 TabAt
LDA.1
INCA
STA.1
BRI tabOfferMatch
tabOfferTake:
; The first match is the answer. Every one after it cuts the answer back to where the two
; stop agreeing, which is all that can be said without guessing which was meant.
SETD.1 TabCount
LDA.1
INCA
STA.1
INIB 0d1
XOR
BRQ tabOfferFirst
CALL tabNarrow
BRI tabOfferSkip
tabOfferFirst:
CALL tabTake
tabOfferSkip:
; Past the end of this candidate, from wherever the comparison stopped.
LDA.3
BRA tabOfferPast
INCD.3
BRI tabOfferSkip
tabOfferPast:
INCD.3
RET
; The candidate DP3 names becomes the answer. DP3 is put somewhere and read back rather than
; walked, because it is the only pointer that survives a call and the caller still needs it.
tabTake:
SETD.1 TabStart
LDD.0.1
SETD.2 TabBest
RSTA
SETD.1 TabBestLen
STA.1
tabTakeNext:
SETD.1 TabBestLen
LDA.1
INIB 0d63
CCF
SUB
BNC tabTakeDone ; As much of a name as there is room for.
LDA.0
BRA tabTakeDone
STA.2
INCD.0
INCD.2
LDA.1
INCA
STA.1
BRI tabTakeNext
tabTakeDone:
RET
; The answer is cut back to where it and the candidate DP3 names stop agreeing.
tabNarrow:
SETD.1 TabStart
LDD.0.1
SETD.2 TabBest
RSTA
SETD.1 TabNarrowAt
STA.1
tabNarrowNext:
SETD.1 TabNarrowAt
LDA.1
SETD.1 TabBestLen
LDB.1
CCF
SUB
BNC tabNarrowDone ; The answer ran out first, so all of it still agrees.
LDA.0
BRA tabNarrowCut ; The candidate ran out, so the answer stops here.
LDB.2
XOR
BNQ tabNarrowCut
INCD.0
INCD.2
SETD.1 TabNarrowAt
LDA.1
INCA
STA.1
BRI tabNarrowNext
tabNarrowCut:
SETD.1 TabNarrowAt
LDA.1
SETD.1 TabBestLen
STA.1
tabNarrowDone:
RET
; ---- The shell's own words ----
;
; Fifteen strings packed end to end, each ending in the zero that says where the next one
; begins. That is why they are laid out that way: a chain of comparisons can be executed and
; cannot be walked.
tabTryNames:
SETD.1 ShellNameCount
LDA.1
SETD.1 TabLeft
STA.1
SETD.3 ShellNames
tabNameNext:
SETD.1 TabLeft
LDA.1
BRA tabNamesDone
DECA
STA.1
CALL tabOffer
BRI tabNameNext
tabNamesDone:
RET
; ---- Showing them when they cannot be narrowed further ----
;
; The line is reprinted underneath afterwards, and where it now BEGINS is asked rather than
; remembered: the prompt has just been printed somewhere else entirely, and the whole of the
; editor's idea of where things are hangs off those two registers.
tabList:
CALL newLine
CALL tabListNames
CALL newLine
CALL sayPrompt
INA 0x03
SETD.1 EditRow
STA.1
INA 0x04
SETD.1 EditColumn
STA.1
RSTA
SETD.1 EditDrawn
STA.1 ; Nothing of the line is on the screen at its new place.
CALL editRedraw
BRI editKey
tabListNames:
SETD.1 ShellNameCount
LDA.1
SETD.1 TabLeft
STA.1
SETD.3 ShellNames
tabListNext:
SETD.1 TabLeft
LDA.1
BRA tabListDone
DECA
STA.1
CALL tabShow
BRI tabListNext
tabListDone:
RET
; DP3 names a candidate. Prints it if it begins with what was typed, and leaves DP3 past its
; zero either way, the same bargain tabOffer makes.
tabShow:
SETD.1 TabStart
STD.3.1
SETD.1 TabWordLen
LDA.1
SETD.1 TabSeen
STA.1
RSTA
SETD.1 TabAt
STA.1
tabShowMatch:
SETD.1 TabSeen
LDA.1
BRA tabShowIt
DECA
STA.1
LDA.3
BRA tabShowSkip
SETD.1 TabHold
STA.1
SETD.1 TabWordAt
LDA.1
SETD.1 TabAt
LDB.1
CCF
ADD
MVQA
CALL tabCharAt
MVQA
SETD.1 TabHold
LDB.1
XOR
BNQ tabShowSkip
INCD.3
SETD.1 TabAt
LDA.1
INCA
STA.1
BRI tabShowMatch
tabShowIt:
SETD.1 TabStart
LDD.0.1
CALL printString
INIA 0x20
OUTA 0x00
OUTA 0x00 ; Two spaces between them, and A already holds one.
tabShowSkip:
LDA.3
BRA tabShowPast
INCD.3
BRI tabShowSkip
tabShowPast:
INCD.3
RET
; ---- A command that did not work ----
;
; The one place a failure is recorded, so that the thing reading lines out of a file can
@@ -5970,6 +6426,38 @@ EditWalkBack:
EditWasControl:
0x00
; ---- What Tab is working on ----
TabWordAt:
0x00
TabWordLen:
0x00
TabCount:
0x00
TabLeft:
0x00
TabSeen:
0x00
TabAt:
0x00
TabHold:
0x00
TabPut:
0x00
TabNarrowAt:
0x00
TabBestLen:
0x00
; Where a candidate is, put down so that a routine which needs to walk it can pick it up
; without moving the pointer its caller is still using.
TabPointer:
0x00 0x00
; And where the candidate being looked at begins, since the comparison walks past it.
TabStart:
0x00 0x00
; The answer so far: the first match, cut back by every one after it to where they agree.
TabBest:
#Reserve 0d64
; ---- What the fault screen is saying ----
;
; Whether this cause names an entry as well as itself, and which one. Only the two faults