; Edit, a line editor for CosmOS. ; ; The first program on this machine that makes a file a person typed. Everything on every ; disk before this one was put there by the host tool. ; ; It is line oriented, in the manner of ed, and that is a deliberate choice rather than a ; limitation of the machine - Snake already draws a whole screen and steers with single ; keys. A full screen editor wants scrolling, a redraw model and cursor arithmetic, none of ; which teaches anything about files, and files are what this exists to exercise. So it ; stays in line mode and reads whole lines, which is what the console does without being ; asked for anything. ; ; l list the whole thing, numbered ; a add lines at the end, until a line that is just a dot ; i put lines in before line n, the same way ; c change line n ; d delete line n ; w write it back ; q stop without writing ; ; ---- How the text is kept ---- ; ; A LINKED LIST OF LINES, not one buffer with newlines in it. Each line is a node holding ; where the next one is, how long it is, and its bytes: ; ; 0 2 where the next line is, or zero ; 2 1 how many bytes this line has ; 3 the bytes ; ; Inserting is then two pointers changed and nothing moved, and so is deleting. With one ; flat buffer both of them would mean shifting everything after the edit, which on a ; machine with no memcpy is a loop over every byte of the rest of the document, run for ; every keystroke's worth of editing. ; ; The price is that DELETED LINES ARE NOT REUSED. A new line always goes at the end of the ; arena, and an unlinked one just sits there. A session that edits heavily uses more room ; than the document needs, and writing the file out and reading it back is what tidies it ; up. That is an honest trade for a program this size, and it is written down here rather ; than left as a surprise. ; ; Two regions are used by arrangement rather than reserved, because reserving them would ; put tens of kilobytes of zeroes into the file for no reason: ; ; 0x4000 the file, on its way in or out ; 0x8000 the arena the lines live in ; ; Nothing is running but this, so both are ours. It is the same arrangement CosmOS makes ; with 0x8000 while it is loading something, for the same reason. #Include services.asm #Program #Base 0x2000 start: SETD.0 FileName INIB 0d23 SWI osArgument SETD.0 FileName LDA.0 BRA noName ; Everything is set here rather than trusted to be zero, since running a program a second ; time does not load it again. RSTA SETD.0 TextHead STA.0 INCD.0 STA.0 SETD.0 ArenaFree INIA 0x80 STA.0 INCD.0 RSTA STA.0 CALL sbfsMount BNQ noDisk CALL loadFile SETD.0 FileName CALL printString SETD.0 CommaText CALL printString CALL countLines MVQA CALL printByteDecimal SETD.0 LinesText CALL printString CALL newLine commandLoop: SETD.0 PromptText CALL printString SETD.0 Command INIB 0d40 CALL readLine ; Running out of typing ends it, the same way it ends the shell. SETD.0 ConsoleEndOfInput LDA.0 BNA quit SETD.0 Command LDA.0 BRA commandLoop ; An empty line asks for nothing. ; Whatever number follows the letter, if there is one. The spaces between the two are ; stepped over first: a number is what somebody typed after "d ", not after "d". SETD.0 Command INCD.0 commandSpaces: LDA.0 INIB 0x20 XOR BNQ commandArgument INCD.0 BRI commandSpaces commandArgument: CALL textNumber MVQA SETD.0 Wanted STA.0 SETD.0 Command LDA.0 INIB 0d108 ; l XOR BRQ doList INIB 0d97 ; a XOR BRQ doAppend INIB 0d105 ; i XOR BRQ doInsert INIB 0d99 ; c XOR BRQ doChange INIB 0d100 ; d XOR BRQ doDelete INIB 0d119 ; w XOR BRQ doWrite INIB 0d113 ; q XOR BRQ quit SETD.0 WhatText CALL printString CALL newLine BRI commandLoop quit: SWI osExit noName: SETD.0 NoNameText CALL printString CALL newLine SWI osExit noDisk: SETD.0 NoDiskText CALL printString CALL newLine SWI osExit ; ---- The commands ---- doList: CALL listLines BRI commandLoop doAppend: CALL countLines MVQA INCA SETD.0 Wanted STA.0 ; Adding at the end is inserting before the line after it. BRI insertLoop doInsert: SETD.0 Wanted LDA.0 BRA insertNeedsLine insertLoop: SETD.0 EnteringText CALL printString SETD.0 Entry INIB 0d80 CALL readLine SETD.0 ConsoleEndOfInput LDA.0 BNA commandLoop ; A line that is just a dot ends it, which is the oldest convention there is for this. SETD.0 Entry SETD.1 DotText CALL textSame BRQ commandLoop SETD.0 Entry CALL makeNode SETD.0 Wanted LDA.0 CALL linkBefore SETD.0 Wanted LDA.0 INCA STA.0 ; The next one goes after the one just put in. BRI insertLoop insertNeedsLine: SETD.0 NeedsLineText CALL printString CALL newLine BRI commandLoop doChange: SETD.0 Wanted LDA.0 BRA insertNeedsLine CALL findLine BNQ noSuchLine SETD.0 EnteringText CALL printString SETD.0 Entry INIB 0d80 CALL readLine SETD.0 ConsoleEndOfInput LDA.0 BNA commandLoop SETD.0 Entry CALL makeNode SETD.0 Wanted LDA.0 CALL linkBefore ; The new one goes in front of the old one, SETD.0 Wanted LDA.0 INCA CALL unlinkLine ; and the old one, now one further along, comes out. BRI commandLoop doDelete: SETD.0 Wanted LDA.0 BRA insertNeedsLine CALL unlinkLine BNQ noSuchLine BRI commandLoop noSuchLine: SETD.0 NoLineText CALL printString CALL newLine BRI commandLoop doWrite: CALL writeFile BNQ writeFailed SETD.0 WrittenText CALL printString SETD.0 WroteSize CALL printWordDecimal SETD.0 BytesText CALL printString CALL newLine BRI commandLoop writeFailed: SETD.0 NoWriteText CALL printString CALL newLine BRI commandLoop ; ---- The list of lines ---- ; DP0 is a string. Puts a node holding it at the end of the arena, and leaves DP3 on it. makeNode: SETD.1 ArenaFree LDD.3.1 PSHD.3 POPD.1 RSTA STA.1 ; Nothing follows it yet. INCD.1 STA.1 INCD.1 PSHD.1 ; Where the length goes, once it is known. INCD.1 RSTB makeNodeLoop: LDA.0 BRA makeNodeEnd STA.1 INCD.0 INCD.1 INCB BRI makeNodeLoop makeNodeEnd: POPD.0 PSHB POPA STA.0 ; How long it turned out to be. INIB 0d3 CCF ADD MVQA SETD.0 ArenaFree CALL addByteToWord RET ; A is a line number. Leaves DP3 on that line and PrevLine on the one before it, which is ; zero when it is the first. Q is zero if there is such a line. findLine: SETD.1 Wanted2 STA.1 INIA 0d1 SETD.1 Counted STA.1 RSTA SETD.1 PrevLine STA.1 INCD.1 STA.1 SETD.1 TextHead LDD.3.1 findLineStep: PSHD.3 POPA POPB OR BRQ findLineMissing SETD.1 Counted LDA.1 SETD.1 Wanted2 LDB.1 XOR BRQ findLineFound PSHD.3 SETD.1 PrevLine POPD.0 STD.0.1 PSHD.3 POPD.0 LDD.3.0 ; On to whatever follows it. SETD.1 Counted LDA.1 INCA STA.1 BRI findLineStep findLineFound: RSTA RSTB CCF ADD RET findLineMissing: RSTA INIB 0d1 CCF ADD RET ; DP3 is a new node and A is the line number it should become. Puts it there. linkBefore: PSHD.3 SETD.1 NewLine POPD.0 STD.0.1 ; The new node, while the old ones are looked through. CALL findLine ; Which may miss, and missing means putting it at the end. ; What the new node should point at is whatever was there, or nothing. SETD.1 NewLine LDD.0.1 BNQ linkBeforeAtEnd PSHD.3 POPD.1 STD.1.0 ; new.next = the line that was there BRI linkBeforeAttach linkBeforeAtEnd: ; Nothing was there, so the new one ends the list and goes after whatever was last. RSTA STA.0 INCD.0 STA.0 SETD.1 NewLine LDD.0.1 linkBeforeAttach: ; And whatever came before now points at the new one. Before the first line, that is ; the head of the list rather than a node. SETD.1 PrevLine LDD.2.1 PSHD.2 POPA POPB OR BRQ linkBeforeHead SETD.1 NewLine LDD.0.1 SETD.1 PrevLine LDD.1.1 STD.0.1 RET linkBeforeHead: SETD.1 NewLine LDD.0.1 SETD.1 TextHead STD.0.1 RET ; A is a line number. Takes it out of the list. Q is zero if there was such a line. unlinkLine: CALL findLine BNQ unlinkMissing ; What follows the one being taken out. PSHD.3 POPD.0 LDD.0.0 SETD.1 PrevLine LDD.2.1 PSHD.2 POPA POPB OR BRQ unlinkHead SETD.1 PrevLine LDD.1.1 STD.0.1 BRI unlinkDone unlinkHead: SETD.1 TextHead STD.0.1 unlinkDone: RSTA RSTB CCF ADD RET unlinkMissing: RSTA INIB 0d1 CCF ADD RET ; Q is how many lines there are. countLines: RSTA SETD.1 Counted STA.1 SETD.1 TextHead LDD.3.1 countStep: PSHD.3 POPA POPB OR BRQ countDone SETD.1 Counted LDA.1 INCA STA.1 PSHD.3 POPD.0 LDD.3.0 BRI countStep countDone: SETD.1 Counted LDA.1 RSTB CCF ADD RET listLines: INIA 0d1 SETD.1 Counted STA.1 SETD.1 TextHead LDD.3.1 listStep: PSHD.3 POPA POPB OR BRQ listDone SETD.0 Counted LDA.0 CALL printByteDecimal SETD.0 ColonText CALL printString PSHD.3 POPD.1 DPUP.1 0d02 LDA.1 SETD.1 Leftover STA.1 PSHD.3 POPD.0 DPUP.0 0d03 SETD.1 Leftover LDA.1 BRA listEmpty listChars: LDA.0 OUTA 0x00 INCD.0 SETD.1 Leftover LDA.1 DECA STA.1 BNA listChars listEmpty: CALL newLine SETD.1 Counted LDA.1 INCA STA.1 PSHD.3 POPD.0 LDD.3.0 BRI listStep listDone: RET ; ---- The file ---- ; Reads the file into lines, if there is one. A name that is not on the disk is a new ; document rather than a mistake, which is what makes this the way to start one. loadFile: SETD.0 FileName CALL sbfsFind BNQ loadNothing SETD.1 0x40 0x00 CALL sbfsRead BNQ loadNothing ; How many bytes came back: the block count is the high byte of the length and the tail ; is the low one, which is how a size is put together everywhere on this disk. SETD.0 SbfsFileBlocks DPUP.0 0d01 LDA.0 SETD.1 ReadLeft STA.1 SETD.0 SbfsFileTail LDA.0 SETD.1 ReadLeft INCD.1 STA.1 SETD.0 0x40 0x00 SETD.1 Entry RSTA SETD.2 EntryLength STA.2 splitStep: ; Anything left? SETD.2 ReadLeft LDA.2 INCD.2 LDB.2 OR BRQ splitLast ; How long the line is so far is kept in memory rather than in B, because comparing ; against a newline needs B and would quietly count the comparison instead of the line. LDA.0 INIB 0d10 XOR BRQ splitLine STA.1 ; A is still the character; an ALU operation does not touch it. INCD.1 SETD.2 EntryLength LDA.2 INCA STA.2 BRI splitOn splitLine: RSTA STA.1 ; The line ends here, so it becomes a string. PSHD.0 ; How far through the file we are. SETD.0 Entry CALL makeNode CALL appendNode POPD.0 SETD.1 Entry RSTA SETD.2 EntryLength STA.2 splitOn: INCD.0 SETD.2 ReadLeft CALL takeOneOff BRI splitStep splitLast: ; A file that does not end in a newline still has a last line in it. SETD.2 EntryLength LDA.2 BRA loadNothing RSTA STA.1 SETD.0 Entry CALL makeNode CALL appendNode loadNothing: RET ; DP3 is a node. Puts it on the end of the list. appendNode: ; The node has to be put somewhere safe first: counting the lines walks the list in DP3, ; which is where the node being added is being held. PSHD.3 CALL countLines MVQA INCA POPD.3 CALL linkBefore RET ; DP2 is a two byte count. Takes one off it. takeOneOff: DPUP.2 0d01 LDA.2 DECA STA.2 BNC takeOneDone ; No borrow, so the high half is untouched. DPDN.2 0d01 LDA.2 DECA STA.2 RET takeOneDone: RET ; Builds the whole document at 0x4000 and saves it. Q is zero if it worked. writeFile: SETD.1 0x40 0x00 SETD.2 TextHead LDD.3.2 writeStep: PSHD.3 POPA POPB OR BRQ writeOut PSHD.3 POPD.0 DPUP.0 0d02 LDA.0 SETD.2 Leftover STA.2 INCD.0 LDA.2 BRA writeBreak writeChars: LDA.0 STA.1 INCD.0 INCD.1 SETD.2 Leftover LDA.2 DECA STA.2 BNA writeChars writeBreak: INIA 0d10 STA.1 INCD.1 PSHD.3 POPD.0 LDD.3.0 BRI writeStep writeOut: ; Where the building stopped says how big it is, with no arithmetic worth the name: the ; buffer starts on a page boundary at 0x4000, so the high byte less 0x40 is the number of ; whole blocks and the low byte is the tail. SETD.2 WroteSize STD.1.2 SETD.0 WroteSize LDA.0 INIB 0x40 CCF SUB MVQA SETD.0 SbfsFileBlocks RSTB STB.0 INCD.0 STA.0 SETD.0 WroteSize INCD.0 LDA.0 SETD.0 SbfsFileTail STA.0 ; The size the file is about to be, said in bytes, before saving changes what these mean. SETD.0 WroteSize LDA.0 INIB 0x40 CCF SUB MVQA SETD.0 WroteSize STA.0 SETD.0 FileName SETD.1 0x40 0x00 CALL sbfsSaveFile RET ; DP0 is a two byte number, A is a byte. Adds the one to the other. addByteToWord: DPUP.0 0d01 LDB.0 CCF ADD STQ.0 DPDN.0 0d01 LDA.0 RSTB ADD STQ.0 RET #Data #Base 0x1000 PromptText: "> " EnteringText: ": " ColonText: ": " CommaText: ", " LinesText: " lines" WrittenText: "written, " BytesText: " bytes" DotText: "." WhatText: "l list, a add, i insert, c change, d delete, w write, q quit" NoNameText: "edit what? try: run edit " NoDiskText: "there is no disk" NoLineText: "there is no such line" NeedsLineText: "which line?" NoWriteText: "it would not write" FileName: #Reserve 0d24 Command: #Reserve 0d41 Entry: #Reserve 0d81 TextHead: 0x00 0x00 ArenaFree: 0x00 0x00 PrevLine: 0x00 0x00 NewLine: 0x00 0x00 Wanted: 0x00 Wanted2: 0x00 Counted: 0x00 Leftover: 0x00 EntryLength: 0x00 ReadLeft: 0x00 0x00 WroteSize: 0x00 0x00 #Include sbfs.asm #Include text.asm #Include console.asm