MODULE editor; (* based on the kilo editor building course*) (* Step 66 *) (*** IMPORTS ***) IMPORT IO, termios, FIO, libc, CharClass, NumberIO, ASCII ; IMPORT STextIO, Strings, SYSTEM, SplitV1, Storage, MemUtils, ProgramArgs; FROM SeqFile IMPORT OpenRead, OpenWrite, OpenResults, Close, ChanId, read, write; FROM IOResult IMPORT ReadResults, ReadResult; FROM TextIO IMPORT ReadString, ReadRestLine, SkipLine, ReadToken; FROM Storage IMPORT ALLOCATE; (*** data ***) TYPE (* display buffer *) abufType = RECORD b : SYSTEM.ADDRESS; len : CARDINAL; END; abufTypePtr = POINTER TO abufType; (* line structure : linked list *) erowPtr = POINTER TO erow; erow = RECORD size : CARDINAL; chars : SYSTEM.ADDRESS; next, previous : erowPtr; END; CONST KILO_VERSION = "0.0.1"; (* escape sequences length *) clearScreenStrLen = 4; cursorHomeStrLen = 3; tildeStrLen = 3; requestCursorPosStrLen = 4; cursorOnStrLen = 6; cursorOffStrLen = 6; eraseLineStrLen = 3; nextLineDownPos1Len = 4; (* fct key mapping*) ARROW_LEFT = 1000; ARROW_RIGHT = 1001; ARROW_UP = 1002; ARROW_DOWN = 1003; HOME_KEY = 1004; END_KEY = 1005; PAGE_UP = 1006; PAGE_DOWN = 1007; DEL_KEY = 1008; VAR editorConfig : RECORD screenrows : CARDINAL; screencols : CARDINAL; posX : CARDINAL; posY : CARDINAL; theTermios : termios.TERMIOS; rows : erowPtr; lastRow : erowPtr; numrows : CARDINAL; rowoff : CARDINAL; END; theRow : erow; theRowPtr : erowPtr; (* termios*) c : CHAR; TCSAFLUSH : INTEGER; (* Split structure *) theStructure : SplitV1.Structure; (* utility *) i : CARDINAL; (* escape sequences *) clearScreenStr : ARRAY [0..3] OF CHAR; cursorHomeStr : ARRAY [0..2] OF CHAR; tildeStr : ARRAY [0..2] OF CHAR; requestCursorPosStr : ARRAY [0..3] OF CHAR; cursorposStr : ARRAY [0..15] OF CHAR; cursorOnStr : ARRAY [0..5] OF CHAR; cursorOffStr : ARRAY [0..5] OF CHAR; eraseLineStr : ARRAY [0..2] OF CHAR; nextLineDownPos1 : ARRAY [0..3] OF CHAR; (* temporary variables *) tmpString : ARRAY [0..15] OF CHAR; tmpString1 : ARRAY [0..15] OF CHAR; tmpString2 : Strings.String1; cc : Strings.String1; (* error and information message string *) message : ARRAY [0..79] OF CHAR; (* cli arguments *) theArgChanId : ChanId; theArgument : ARRAY [0..20] OF CHAR; theFileName : ARRAY [0..20] OF CHAR; argumentNumber : CARDINAL; (******************** Utilities *****************) PROCEDURE Card2Str ( x : CARDINAL; VAR str : ARRAY OF CHAR); BEGIN IF x < 10 THEN NumberIO.CardToStr(x,1,str); ELSIF x < 100 THEN NumberIO.CardToStr(x,2,str); ELSE NumberIO.CardToStr(x,3,str); END; END Card2Str; (******************** Buffer ******************) PROCEDURE abufInit( VAR ptr : abufTypePtr); BEGIN Storage.ALLOCATE(ptr,SYSTEM.TSIZE(abufType) ); ptr^.b := NIL; ptr^.len := 0; END abufInit; PROCEDURE abAppend (VAR ptr : abufTypePtr; theSourceAdress: SYSTEM.ADDRESS; theSourceLen : CARDINAL ); BEGIN IF ptr <> NIL THEN IF ptr^.b = NIL THEN Storage.ALLOCATE(ptr^.b,theSourceLen); ELSE Storage.REALLOCATE( ptr^.b,ptr^.len + theSourceLen); END; MemUtils.MemCopy(theSourceAdress,theSourceLen, SYSTEM.ADDADR(ptr^.b,ptr^.len)); ptr^.len := ptr^.len + theSourceLen; END; END abAppend; PROCEDURE abFree (VAR ptr : abufTypePtr); BEGIN Storage.DEALLOCATE(ptr^.b, ptr^.len); Storage.DEALLOCATE(ptr,SYSTEM.TSIZE(abufType)); END abFree; (*** Terminal ***) PROCEDURE CtrlKey(c: CHAR) : CARDINAL; VAR n : BITSET; BEGIN n := BITSET(ORD(c)); n := n * BITSET(1FH); RETURN CARDINAL(n) END CtrlKey; PROCEDURE die (s : ARRAY OF CHAR); BEGIN libc.write(FIO.StdOut,SYSTEM.ADR(cursorHomeStr), cursorHomeStrLen); libc.write(FIO.StdOut,SYSTEM.ADR(clearScreenStr), clearScreenStrLen); libc.perror(s); FOR i := 0 TO Strings.Length(s) DO IO.Write(s[i]) END; HALT END die; PROCEDURE disablerawMode() : INTEGER; VAR result : INTEGER; BEGIN IO.BufferedMode(0,TRUE); IO.BufferedMode(1,TRUE); result := termios.tcsetattr(FIO.StdIn, TCSAFLUSH, editorConfig.theTermios); IF result = -1 THEN editorConfig.theTermios := termios.KillTermios(editorConfig.theTermios); die("tcsetattr") ELSE RETURN result END; END disablerawMode; PROCEDURE enablerawMode; BEGIN libc.atexit(disablerawMode); editorConfig.theTermios := termios.InitTermios(); IF termios.tcgetattr(FIO.StdIn, editorConfig.theTermios) = -1 THEN die("tcgetattr") END; IO.UnBufferedMode(0,TRUE); IO.UnBufferedMode(1,TRUE); END enablerawMode; PROCEDURE editorReadKey() : CARDINAL; VAR c : CHAR; d : CHAR; num : CARDINAL; (* The Home key could be sent as [1~, [7~, [H, or OH (this is the letter O followed by H). Similarly, the End key could be sent as [4~, [8~, [F, or OF (this is the letter O followed by F). *) BEGIN IO.Read(c); IF c = ASCII.esc THEN IO.Read(c); (* mode := command; *) IF c = "[" THEN IO.Read(c); IF (c > "0") AND (c <"9") THEN IO.Read(d); IF d = "~" THEN CASE c OF "4", "8" : num := END_KEY; | "1", "7" : num := HOME_KEY; | "5" : num := PAGE_UP; | "6" : num := PAGE_DOWN;| "3" : num := DEL_KEY END; END; ELSIF (c = "A") OR (c = "B") OR (c = "C") OR (c = "D") OR (c = "F") OR (c = "H") THEN CASE c OF "A" : num := ARROW_UP; | "B" : num := ARROW_DOWN ; | "C" : num := ARROW_RIGHT; | "D" : num := ARROW_LEFT; | "F" : num := END_KEY; | "H" : num := HOME_KEY; END; ELSIF c = "O" THEN IO.Read(c); CASE c OF "F" : num := END_KEY; | "H" : num := HOME_KEY; END; ELSE message := ("key not recognized"); END; (* mode := move; *) (* OF OH à voir !*) END; RETURN num; ELSE (* mode := edit; *) RETURN ORD(c) END; END editorReadKey; (*** row operations ***) PROCEDURE editorAppendRow(VAR line : ARRAY OF CHAR); VAR size : CARDINAL; BEGIN NEW(theRowPtr); size := Strings.Length(line); theRowPtr^.size := size; Storage.ALLOCATE(theRowPtr^.chars, size); MemUtils.MemCopy(SYSTEM.ADR(line),size,theRowPtr^.chars); (* if no lines were allocated *) IF editorConfig.rows = NIL THEN theRowPtr^.next := NIL; theRowPtr^.previous := editorConfig.rows; editorConfig.rows := theRowPtr; ELSE (* there are already some lines in the structure *) theRowPtr^.next := NIL; theRowPtr^.previous := editorConfig.lastRow; editorConfig.lastRow^.next := theRowPtr; END; editorConfig.lastRow := theRowPtr; INC(editorConfig.numrows); END editorAppendRow; (******************** file I/O ***************) PROCEDURE editorOpen( fileName : ARRAY OF CHAR); VAR theChanId : ChanId; theResult : OpenResults; line : ARRAY[0..255] OF CHAR; BEGIN (* PROCEDURE OpenRead (VAR cid: ChanId; name: ARRAY OF CHAR; flags: FlagSet; VAR res: OpenResults); *) OpenRead(theChanId, fileName, read, theResult); (* test du résultat*) IF theResult = noSuchFile THEN die("No such file") ELSIF theResult=opened THEN (* reading the lines ... *) (* first line *) line := ""; ReadString(theChanId, line); (* testing the result *) WHILE ReadResult(theChanId) <> endOfInput DO (* testing end of line *) IF ReadResult(theChanId) = endOfLine THEN (* we have to 'skip' the endOfLine ... so strange *) SkipLine(theChanId); END; editorAppendRow(line); (* reading next line *) ReadString(theChanId, line); END; ELSE die("Error opening the file"); END; Close(theChanId); END editorOpen; PROCEDURE editorMoveCursor(c : CARDINAL); BEGIN WITH editorConfig DO CASE c OF ARROW_UP, ORD("e") : IF posY > 1 THEN DEC(posY) END; | ARROW_DOWN, ORD("x") : IF posY < (screenrows - 1) THEN INC(posY) END; | ARROW_LEFT, ORD("s") : IF posX > 1 THEN DEC(posX) END; | ARROW_RIGHT, ORD("d") : IF posX < (screencols - 1) THEN INC(posX) END; | PAGE_UP : posY := 1; | PAGE_DOWN : posY := screenrows; | HOME_KEY : posX := 1; | END_KEY : posX := screencols; END; END END editorMoveCursor; PROCEDURE editorProcessKeypress; VAR c : CARDINAL; BEGIN c := editorReadKey(); CASE c OF 17 : libc.write(FIO.StdOut,SYSTEM.ADR(cursorHomeStr), cursorHomeStrLen); libc.write(FIO.StdOut,SYSTEM.ADR(clearScreenStr), clearScreenStrLen); HALT; | ORD("e"), ORD("x"), ORD("s"), ORD("d"), HOME_KEY, END_KEY, PAGE_UP, PAGE_DOWN, ARROW_UP, ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT : editorMoveCursor(c); END END editorProcessKeypress; (*** Output ***) PROCEDURE editorDrawRows(VAR ptr : abufTypePtr); CONST tilde = "~"; VAR y : CARDINAL; welcome : ARRAY [0..79] OF CHAR; welcomeLen : CARDINAL; blanks : CARDINAL; blanksStr : ARRAY[0..79] OF CHAR; blankStrLength : CARDINAL; len : CARDINAL; BEGIN (* Creation of the about text *) welcome := "Kilo editor -- version "; Strings.Concat(welcome,KILO_VERSION,welcome); welcomeLen := Strings.Length(welcome); Strings.Concat(welcome,nextLineDownPos1,welcome); (* centering the welcome text *) blanks := (editorConfig.screencols - welcomeLen) /2; cc[0] := " "; blanksStr := ""; FOR i := 0 TO blanks DO Strings.Concat(blanksStr,cc,blanksStr); END; blankStrLength := Strings.Length(blanksStr); (* display of the page *) theRowPtr := editorConfig.rows; FOR y := 1 TO editorConfig.screenrows -1 DO (* for all lines except the last one *) IF y > editorConfig.numrows THEN IF ((y = (editorConfig.screenrows / 3)) AND (editorConfig.screencols > welcomeLen)) AND (editorConfig.numrows = 0) THEN abAppend(ptr,SYSTEM.ADR(blanksStr), blankStrLength); abAppend(ptr,SYSTEM.ADR(welcome),welcomeLen ); abAppend(ptr,SYSTEM.ADR(eraseLineStr),eraseLineStrLen ); abAppend(ptr,SYSTEM.ADR(nextLineDownPos1),nextLineDownPos1Len ); ELSE abAppend(ptr,SYSTEM.ADR(tilde), 1); abAppend(ptr,SYSTEM.ADR(eraseLineStr),eraseLineStrLen ); abAppend(ptr,SYSTEM.ADR(nextLineDownPos1),nextLineDownPos1Len ); END; ELSE len := theRowPtr^.size; IF len > editorConfig.screencols THEN len := editorConfig.screencols END; (* displaying the text lines *) abAppend(ptr,theRowPtr^.chars,len); theRowPtr := theRowPtr^.next; abAppend(ptr,SYSTEM.ADR(eraseLineStr),eraseLineStrLen ); abAppend(ptr,SYSTEM.ADR(nextLineDownPos1),nextLineDownPos1Len ); END; END; (* last line *) abAppend(ptr,SYSTEM.ADR(tilde), 1); abAppend(ptr,SYSTEM.ADR(eraseLineStr),eraseLineStrLen ); abAppend(ptr,SYSTEM.ADR(nextLineDownPos1),nextLineDownPos1Len ); END editorDrawRows; PROCEDURE editorRefreshScreen; VAR ptr : abufTypePtr; BEGIN abufInit(ptr); abAppend(ptr,SYSTEM.ADR(cursorOffStr), cursorOffStrLen); abAppend(ptr,SYSTEM.ADR(cursorHomeStr), cursorHomeStrLen); editorDrawRows(ptr); (* cursorpos(1,1,FALSE); *) cursorpos(editorConfig.posX,editorConfig.posY,FALSE); abAppend(ptr,SYSTEM.ADR(cursorposStr), Strings.Length(cursorposStr)); abAppend(ptr,SYSTEM.ADR(cursorOnStr), cursorOnStrLen); libc.write(FIO.StdOut,ptr^.b,ptr^.len); abFree(ptr); END editorRefreshScreen; PROCEDURE requestCursorPosition(VAR x,y : CARDINAL); VAR c : CHAR; str : ARRAY [0..15] OF CHAR; BEGIN str := ""; SplitV1.InitStructure(theStructure); libc.write(FIO.StdOut,SYSTEM.ADR(requestCursorPosStr),requestCursorPosStrLen); (* getting answer *) str := ""; REPEAT IO.Read(c); cc[0] := c; Strings.Concat(str,cc,str); UNTIL c = "R"; Strings.Delete(str,0,2); Strings.Delete(str,Strings.Length(str) -1 ,1); SplitV1.SplitStr(str, ";", theStructure); NumberIO.StrToCard(theStructure[0].element,y); NumberIO.StrToCard(theStructure[1].element,x); END requestCursorPosition; PROCEDURE cursorpos(x,y : CARDINAL; emit : BOOLEAN); BEGIN cursorposStr := ""; Strings.Concat(cursorposStr, ASCII.esc,cursorposStr); Strings.Concat(cursorposStr, "[",cursorposStr); tmpString := ""; Strings.Concat(tmpString,cursorposStr, tmpString); tmpString1 := ""; Card2Str(y,tmpString1); Strings.Concat(tmpString,tmpString1, tmpString); tmpString2[0] := ";"; Strings.Concat(tmpString, tmpString2,tmpString); tmpString1 := ""; Card2Str(x,tmpString1); Strings.Concat(tmpString,tmpString1, tmpString); tmpString2[0] := "H"; Strings.Concat(tmpString, tmpString2,tmpString); cursorposStr := tmpString; editorConfig.posX := x; editorConfig.posY := y; IF emit THEN libc.write(FIO.StdOut,SYSTEM.ADR(tmpString),Strings.Length(tmpString)); END; END cursorpos; PROCEDURE getWindowSize(VAR y : CARDINAL; VAR x: CARDINAL); BEGIN cursorpos(999,999, TRUE); requestCursorPosition(x,y); END getWindowSize; (*** Input ***) (*** Init ***) PROCEDURE InitScreenEscapes; VAR temp : ARRAY[0..10] OF CHAR; BEGIN cursorHomeStr := ""; Strings.Concat(cursorHomeStr,ASCII.esc,cursorHomeStr); Strings.Concat(cursorHomeStr,"[H",cursorHomeStr); clearScreenStr := ""; Strings.Concat(clearScreenStr,ASCII.esc,clearScreenStr); Strings.Concat(clearScreenStr,"[2J",clearScreenStr); tildeStr[0] := "~"; tildeStr[1] := CHR(10); tildeStr[2] := CHR(13); cursorposStr := ""; Strings.Concat(cursorposStr,ASCII.esc,cursorposStr); Strings.Concat(cursorposStr,"[",cursorposStr); requestCursorPosStr := ""; Strings.Concat(requestCursorPosStr,ASCII.esc,requestCursorPosStr); Strings.Concat(requestCursorPosStr,"[6n",requestCursorPosStr); cursorOnStr := ""; Strings.Concat(cursorOnStr,ASCII.esc,cursorOnStr); Strings.Concat(cursorOnStr,"[?25h",cursorOnStr); cursorOffStr := ""; Strings.Concat(cursorOffStr,ASCII.esc,cursorOffStr); Strings.Concat(cursorOffStr,"[?25l",cursorOffStr); eraseLineStr := ""; Strings.Concat(eraseLineStr,ASCII.esc,eraseLineStr); Strings.Concat(eraseLineStr,"[K",eraseLineStr); (* ESC[#E moves cursor to beginning of next line, # lines down *) nextLineDownPos1 := ""; Strings.Concat(nextLineDownPos1,ASCII.esc,nextLineDownPos1); Strings.Concat(nextLineDownPos1,"[1E",nextLineDownPos1); END InitScreenEscapes; PROCEDURE InitEditor; BEGIN TCSAFLUSH := termios.tcsflush (); InitScreenEscapes; editorConfig.posX := 1; editorConfig.posY := 1; getWindowSize(editorConfig.screenrows, editorConfig.screencols); editorConfig.posX := 1; editorConfig.posY := 1; editorConfig.numrows := 0; editorConfig.rows := NIL; editorConfig.rowoff := 1; libc.write(FIO.StdOut,SYSTEM.ADR(clearScreenStr),4); END InitEditor; BEGIN enablerawMode; InitEditor; editorRefreshScreen; (* fetching the 2nd argument (fileName) *) theFileName := ""; theArgChanId := ProgramArgs.ArgChan (); argumentNumber := 0; WHILE ProgramArgs.IsArgPresent() DO INC(argumentNumber); ReadToken(ProgramArgs.ArgChan(), theArgument); IF argumentNumber = 2 THEN theFileName := theArgument; END; ProgramArgs.NextArg (); END; IF argumentNumber >= 2 THEN editorOpen(theFileName); END; LOOP editorRefreshScreen; editorProcessKeypress; END; END editor.