diff --git a/Programs/CosmOS/README.md b/Programs/CosmOS/README.md index ca4f397..e232bec 100644 --- a/Programs/CosmOS/README.md +++ b/Programs/CosmOS/README.md @@ -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 diff --git a/Programs/CosmOS/Source/cosmos.asm b/Programs/CosmOS/Source/cosmos.asm index 73ab0be..3715b43 100644 --- a/Programs/CosmOS/Source/cosmos.asm +++ b/Programs/CosmOS/Source/cosmos.asm @@ -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 diff --git a/Source/Assembler/secondPass.h b/Source/Assembler/secondPass.h index f689349..f2a2d5c 100644 --- a/Source/Assembler/secondPass.h +++ b/Source/Assembler/secondPass.h @@ -15,7 +15,12 @@ // share one namespace: a name may only be defined once across the whole assembly. So this // is not the size of one file but the size of a program and its libraries together, and // CosmOS with its four libraries went past 256 while still being a small system. -#define MAX_LABELS 1024 +// +// AND PAST 1024 the day the shell learned to finish a word somebody had started. Doubled +// rather than nudged: a ceiling reached once is a ceiling that will be reached again, and +// the table is pointers into source that is already in memory - 2048 of them is sixteen +// kilobytes on a host with gigabytes of it. +#define MAX_LABELS 2048 #define MAX_VECTORS 256 typedef struct { diff --git a/SplitBit Test Manual.md b/SplitBit Test Manual.md index e6f5646..b2dea2f 100644 --- a/SplitBit Test Manual.md +++ b/SplitBit Test Manual.md @@ -79,7 +79,7 @@ from `make`, not from here. ### 1. Recorded output `Tests/run.sh` assembles each program named in `Tests/manifest`, runs it, and compares -everything it printed against a file in `Tests/expected`. 197 tests, of which 135 run, 35 +everything it printed against a file in `Tests/expected`. 198 tests, of which 136 run, 35 only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image given at all. diff --git a/Tests/expected/cosmosTab.out b/Tests/expected/cosmosTab.out new file mode 100644 index 0000000..33a2965 --- /dev/null +++ b/Tests/expected/cosmosTab.out @@ -0,0 +1,34 @@ +CosmOS +> +> ececho one +one +> mkmkdir /made-by-tab +made +> d +dir do drive delete +> d +I do not know: d +> zzzx +I do not know: zzzx +> echo here +here +> help  help  +dir list what is on the disk +load read a program off the disk +run [words] start what was loaded, and tell it those words + [words] look where you are and then in /Apps, and start that +cd [path] go to a directory, or to the root with nothing after it +mkdir make a directory +rmdir remove an empty one +do run the lines in a file, which must start with #! +echo [words] say them +clear empty the screen +delete take it off the disk +rename call it something else +monitor look at memory, change it, and jump into it +help this +exit stop, or leave the monitor if you are in it +> exit +halted +Execution halted. +[exit 0] diff --git a/Tests/input/cosmosTab.in b/Tests/input/cosmosTab.in new file mode 100644 index 0000000..4308489 --- /dev/null +++ b/Tests/input/cosmosTab.in @@ -0,0 +1,8 @@ + +ec one +mk /made-by-tab +d +zzz x +echo he re +help +exit diff --git a/Tests/manifest b/Tests/manifest index 462f18c..dff4fcc 100644 --- a/Tests/manifest +++ b/Tests/manifest @@ -375,6 +375,20 @@ cosmosEditKeys | CosmOS/Source/cosmos.asm | run | cosmosEdi # the tail of the long one on the screen unless something covers it, and one space - which # is all a Delete ever needs - covers nothing. cosmosHistory | CosmOS/Source/cosmos.asm | run | cosmosHistory.in | - | disks/cosmos.img +# Tab finishing a word somebody started, which is a thing an eight bit shell could not do +# for a structural reason rather than a hard one: it never saw the keystroke, so there was no +# moment at which half a word existed and the system knew. +# +# Every case is here. One match goes in with a space after it. Four that agree on nothing +# more are listed and the line put back underneath - and the line coming back is the half +# worth watching, because the prompt has been reprinted somewhere else and the editor's idea +# of where the line starts has to be asked again rather than remembered. Nothing typed and +# nothing matching both do nothing quietly, and a second Tab later in the line does nothing +# because finishing an argument is not built yet. +# +# The last line is the one that says it works on what is actually there rather than on what +# was typed: two backspaces take "help" down to "he", and Tab finishes it again. +cosmosTab | CosmOS/Source/cosmos.asm | run | cosmosTab.in | - | disks/cosmos.img # The same editing offered to a PROGRAM, through osReadLine. Edit reads its lines that way, # so a word typed with two letters the wrong way round is put right without starting the line # again - which is the whole of what A4 buys.