Your Ad Here

How To Write An Editor


by: Aiwendil
Published: 2001
©opyright 2009 by Friends of FPC

Introduction


This will explain one way to write an texteditor that will compile on most platforms (as long as it has the units crt and dos (dos might be replaced with something more appropriate for the platform that is intended as target, but then all file-operations will change slightly)).

This will be made in an way that is quite easy to update, expand, and most important, it will be made in several parts.. all written when I get around to it. :)

And the normal legal horrors.

ALL INFORMATION (INCLUDING THE CODE) IN THE THIS DOCUMENT AND THE FOLLOWING PARTS ARE PROVIDED AS-IS WITHOUT ANY FORM OF WARRANTY. I ASSUME NO LIABILITY WHATSOEVER FOR ANY DAMAGES THAT MIGHT ARISE FROM THE USE OF THIS INFORMATION (AND CODE). USE AT YOUR OWN RISK!

ALL TRADEMARKS BELONG TO THEIR RESPECTIVE HOLDER

Use and distribution of this document and the accompanying parts is free as long as you comply to the following rules.

  • Any modification of the text for increased readability and translations are allowed as long as the changes are clearly stated and the legal notices stays intact.
  • If you use code from this document and it's accompanying documents then you must somewhere add that you have used an document written by Aiwendil.

First Steps


First of all we should take a look at what we need to accomplish, what we want to accomplish will be a later matter.

What we need are:

  • Buffers
    • Create buffer
      • Get memory
      • Update chain
    • Destroy buffer
      • Copy active buffer-pointer
      • Remove buffer from chain
      • Free old buffer memory through copy
    • Write buffer
    • Read buffer
  • Screen-updater
    • Determine which kind of update
      • Complete?
      • Partial
  • File-operations
    • Load file
      • Open file
      • Read from file to line
      • Create buffer
      • Put line in buffer
      • Loop to 'load file' if there was no error
    • Save file
      • Check if file exist
        • If it does then ask if it should be overwritten
    • Create/Rewrite file
      • Put buffer in line
      • Write line to file
      • Loop to 'save file' if there was no error
  • Keyboard-handler
    • Normal chars
      • Translate
        • Buffer-operations
        • Resize buffer
        • Add char to buffer
      • Loop to 'keyboard-handler'
    • Commands
      • Identify commands
        • Actions
        • Loop to 'keyboard-handler' if not prevented

As you can see most of the program is already done by now all we have to do is to sort it and put the code down in a file, a pure number-cruncher in other words.

The two approaches

There are two ways one can write this, either in a large file (or several and use includes) or split it up into several units/libraries. I will use the later of the approaches since it will keep the code clear and easy to find, the drawback with this is that it is a bit slower (you will most likely never notice the difference when it comes to speed).

The files

I will split it up into the following files:

  • buffers.pas - buffer-operations.
  • screen.pas - screen routines, mainly the updater.
  • fileops.pas - file-operations, load, save, and so on
  • keyboard.pas - keyboard-handling and routines.
  • types.pas - shared types and constants.

As you can see I will have one for for each type of task performed, this will force you to write readable code as well as keeping the procedures small and specialized, this will give you an almost forgotten way to really optimize code and it will be easier to "tap into" a certain procedure/function to see how many times it's called, and if it's called at all. One important thing with this approach is that you should smartlink everything (that will make sure that only the needed code will be compiled into the complete program).

Oh well, that's enough boring stuff for now. Time to get on to the fun parts

Once upon a time, there was an empty space.. that we shall fill with code

A note for the following sections, I will include code quite often and you could most likely copy and paste it, but if you are unsure of what a certain thing do then look it up and try to write the thing yourself.

A second note for the following sections, I will discard all memory errors which will make the program easier to write and understand but it will also be more likely to crash instead of reporting an error if the memory would run out.

Let's start with buffers, there are several nice ways to do this (and plenty of ugly ways), I prefer to use linked lists, and when I favor speed over memory I use double linked lists and also when I'm too lazy to optimize (this tutor will use double linked lists).

Declare this one in the interface part of the unit buffers.pas since that will force all your other procedures to be aware of this unit (and if you only use the procedures in this unit for all buffer manipulation it will make debugging easier).

First hard decision shall we use pascal-style or asciiz style. Both have their advantages. With pascal it's easy but limited, with asciiz it is hard but only the memory available sets the limit. I will use pascal-style in this tutor since they are easier to work with, if you want to convert it to asciiz take a look at the unit strings that comes with your compiler.

Each entry in the chain with buffers should have the following, a var to hold the data (a pointer), a pointer to the next object in the chain, and an pointer to the previous object in the chain.

 TYPE pBuffer = ^tBuffer;
      tBuffer = RECORD
                   Data : ^STRING;
                   Next : pBuffer;
                   Prev : pBuffer;
                END;

The reason that I made data as a pointer is that as long as the string are less than 250 chars we will save memory, and how often do you read documents that have more than about 90% of the lines longer than that? :)

Note however that pBuffer and tBuffer must both be in the same TYPE block (put a TYPE in front of tBuffer and you will get a fun error) and also, they belong in buffers.pas and NOT types.pas.

Next up we have a fun task, to allocate a buffer entry. This procedure will be smaller then I usually make stuff but it will also be more general which will make it easier to use it for all sorts of things.

This procedure will only allocate and set up all the values to defaults.

 PROCEDURE AllocBuffer(VAR Buffer:pBuffer);
 BEGIN
    New(Buffer);
    WITH Buffer^ DO BEGIN
       Data:=NIL;
       Next:=NIL;
       Prev:=NIL;
    END;
 END;

And now for the deallocation of the buffer, this function will return false if it is Data is allocated, on success it will return true.

 FUNCTION DeAllocBuffer(VAR Buffer:pBuffer):BOOLEAN;
 BEGIN
    DeAllocBuffer:=True;
    IF Buffer<>NIL THEN 
       IF Buffer^.Data<>NIL THEN DeAllocBuffer:=False
       ELSE Dispose(Buffer);
 END;

Now we have the routines to create and destroy buffers, that is all fine but now, is there anything we can do with it, yes - an entire program :)

Now we can play around with the Data in tBuffer, this is where the fun begins.. see if you can keep up with me :)

Will perform no errorchecking so make sure that Data points to NIL and that Buffer points to a pBuffer.

 PROCEDURE AllocBufferData(VAR Buffer:pBuffer;S:STRING);
 BEGIN
    WITH Buffer^ DO BEGIN
       GetMem(Data,Length(S)+1);
       Data^:=S;
    END;
 END;

And now for the dealloc, partial error-checking, it will se if data points otherwise than nil and only proceed if it does.

 PROCEDURE DeAllocBufferData(VAR Buffer:pBuffer);
 BEGIN
    WITH Buffer^ DO BEGIN
       IF Data<>NIL THEN FreeMem(Data,Length(Data^)+1);
       Data:=NIL;
    END;
 END;

And a function to get the data in Data in a Buffer.

 FUNCTION GetBufferData(VAR Buffer:pBuffer):STRING;
 BEGIN
    WITH Buffer^ DO 
       IF Data<>NIL THEN GetBufferData:=Data^
       ELSE GetBufferData:=;
 END;

And now to resize the Buffer.Data, this is useful if you have edited a line and don't want to update the entire chain and all such.

 PROCEDURE ResizeBufferData(VAR Buffer:pBuffer;S:STRING);
 BEGIN
    DeAllocBufferData(Buffer);
    AllocBufferData(Buffer,S);
 END;

This is another main advantage with keeping stuff small and simple, you can reuse large parts of your code and thanks to that the actual time typing and the amount of lines will be reduced.

And now for another fun thing with pointers, double-linked lists. Time to actually have the list properly initialized and set up in a usable and still easy to use for, declare this type in the interface part of buffer.pas as well.

 TYPE tBufferList = RECORD
                       FirstBuffer   : pBuffer;
                       LastBuffer    : pBuffer;
                       CurrentBuffer : pBuffer;
                    END;

As you can see all three of them are of the type pBuffer, but let's briefly go through what their purposes are. FirstBuffer will point to the first buffer in the list, this is mainly to speed things up, it is actually not needed but will as I said speed things up and also make coding a lot easier.

LastBuffer is like FirstBuffer but points to the last buffer in the list.

CurrentBuffer is the important one, this one if properly maintained will point to the current buffer and will be one of the most used pointers in the program.

And we need a procedure to set the tBufferList to some nice defaults.

 PROCEDURE InitBufferList(VAR BufferList:tBufferList);
 BEGIN
    WITH BufferList DO BEGIN
       FirstBuffer:=NIL;
       LastBuffer:=NIL;
       CurrentBuffer:=NIL;      
    END;
 END;

And also needed are routines to put pBuffers into the list. I never said that pointers are easy things to work with, they are hard, most programmers learn to either hate them or love them. :)

This one is relative to the CurrentBuffer, the Buffer on among the parameters are the Buffer to add to the BufferList.

 PROCEDURE AddBufferToList(VAR BufferList:tBufferList;VAR Buffer:pBuffer);
 BEGIN
    IF Buffer=NIL THEN Exit;
    WITH BufferList DO WITH Buffer^ DO BEGIN
       Prev:=CurrentBuffer;
       IF CurrentBuffer<>NIL THEN BEGIN
          Next:=CurrentBuffer^.Next;
          CurrentBuffer^.Next:=Buffer;
       END ELSE Next:=NIL;
       IF FirstBuffer=NIL THEN FirstBuffer:=Buffer;
       IF LastBuffer=NIL THEN LastBuffer:=Buffer;
       IF LastBuffer=CurrentBuffer THEN LastBuffer:=Buffer;
       CurrentBuffer:=Buffer;
    END;
 END;

And to remove it from the list, the Buffer on the list is the buffer to remove.

 PROCEDURE RmBufferFromList(VAR BufferList:tBufferList;VAR Buffer:pBuffer);
 BEGIN
    IF Buffer=NIL THEN Exit;
    WITH BufferList DO
       WITH Buffer^ DO BEGIN
          IF Next=NIL THEN LastBuffer:=Prev
             ELSE Next^.Prev:=Prev;
          IF Prev=NIL THEN FirstBuffer:=Next
             ELSE Prev^.Next:=Next;
          IF CurrentBuffer=Buffer THEN
             IF Next<>NIL THEN CurrentBuffer:=Next
             ELSE CurrentBuffer:=Prev;
       END;
 END;

And some only to provide a nice programmers interface (for the sake of completness).

 FUNCTION BufferNext(VAR Buffer:pBuffer):pBuffer;
 

Returns what's in the var Next in Buffer

BEGIN IF Buffer<>NIL THEN BufferNext:=Buffer^.Next ELSE BufferNext:=NIL; END; FUNCTION BufferPrev(VAR Buffer:pBuffer):pBuffer;

Returns what's in the var Prev in Buffer

BEGIN IF Buffer<>NIL THEN BufferPrev:=Buffer^.Prev ELSE BufferPrev:=NIL; END;

And one in order to update the BufferList

 FUNCTION GoBufferList(VAR BufferList:tBufferList;Next:Boolean):pBuffer;
 

Will update CurrentBuffer to Next/Prev and also return the new value, if the Next/Prev is NIL this one will return NIL but not update CurrentBuffer. If Next is true the it will go Next other wise it will go Prev

LABEL _NIL; BEGIN WITH BufferList DO BEGIN IF CurrentBuffer=NIL THEN GOTO _NIL; CASE Next OF True : BEGIN IF CurrentBuffer^.Next=NIL THEN GOTO _NIL; CurrentBuffer:=CurrentBuffer^.Next; END; False : BEGIN IF CurrentBuffer^.Prev=NIL THEN GOTO _NIL; CurrentBuffer:=CurrentBuffer^.Prev; END; END; GoBufferList:=CurrentBuffer; END; Exit; _NIL: GoBufferList:=NIL; END;

And also we need something to make it easier to add strings to the BufferList so here comes a small piece of code for it.

 PROCEDURE BufferAddStr(VAR BufferList:tBufferList;S:STRING);
 VAR Buf : pBuffer;
 BEGIN
    AllocBuffer(Buf);
    AllocBufferData(Buf,S);
    AddBufferToList(BufferList,Buf);
 END;

Users, no matter how annoying, are always to take into consideration,

The screen, here we have a thing that actually are tricky in Linux since a screen can be just about anywhere from 1
  • 2 to 255
  • 255 chars, I will assume that you will solve that problem for yourself and also another nice problem, that Linux usually tend to expand tabs and such itself.

screen.pas are what we are working with in this part unless otherwise specified. also, it is assumed that you use the unit buffers specified in part1 and the unit CRT or equivalent, what we need are the routines gotoxy and clrscr.

First we need to define a type for windowed controls, in dos this step is skipped by most programmers and inserted later with a great effort, in Linux you almost must implement this right away since the size of the screen can vary a lot, using this approach will also make it lots easier to implement multiple document interface.

 TYPE tWindow = RECORD
                   XPos : Byte;
                   YPos : Byte;
                   XLength : Byte;
                   YLength : Byte;
                   BufferList : tBufferList;
                END;

There, it is pretty straightforward, I know that YLength would be more correct to call YHeight instead but I prefer to keep the code uniform instead of correct according to some rules one don't really have to pay attention to while coding.

First we need a procedure to init the tWindow to nice defaults;

 PROCEDURE InitWindow(VAR Window:tWindow);
 BEGIN
    WITH Window DO BEGIN
       XPos := 1;
       YPos := 1;
       XLength := 80;
       YLength:=25;
       InitBufferList(BufferList);
    END;
 END;

Ok, that wasn't so hard, and I doubt you need to have that one explained.

Now, time to make the partial update-routine, this one will be called upon frequently so here it is to favor speed over memory.

 PROCEDURE WindowUpdate(CurX,CurY,X,XLen,Depth:Byte;Dir:Boolean;
                        VAR Buffer:pBuffer);
 

CurX/CurY is the row/column to start writing on. X is on which position in Data to start with. XLen is the number of chars to write for each line. Depth is the number of levels to go. Dir is which direction to go. True = Next, False = Prev. Buffer is the the first Buffer to work with

VAR TmpBuffer : pBuffer; B : Byte; S : STRING; BEGIN IF (Depth=0) OR (X=0) OR (XLen=0) THEN Exit; TmpBuffer:=Buffer; FOR B:=1 TO Depth DO BEGIN GotoXY(CurX,CurY+B-1); FillChar(S[1],XLen,' '); IF TmpBuffer<>NIL THEN S:=Copy(GetBufferData(TmpBuffer),X,XLen); S[0]:=Chr(XLen); Write(S); IF TmpBuffer<>NIL THEN IF Dir THEN TmpBuffer:=BufferNext(TmpBuffer) ELSE TmpBuffer:=BufferPrev(TmpBuffer); END; END;

The above procedure will have a rather fun bug on many but not all systems, let's see if you can figure it out and catch it :)

And now an updater for the entire window

 PROCEDURE UpdateEntireWindow(X:Byte;Dir:Boolean;VAR Buffer:pBuffer;
                              Window:tWindow);
 
 

for explanation of parameters see WindowUpdate. Window is the tWindow we have to work with

BEGIN WITH Window DO WindowUpdate(XPos, YPos, X, XLength, YLength, True, Buffer); END;

Hard drives, no matter how fast, are always slow compared to RAM.

File operations, operations with files, are nice if you at some point would like to save your work. This is a part that can cause really big problems for a programmer since there are too many different ways to store information in a file. Since this editor is intended for multiple platforms and to write files to use with fpc I will make the example code use hard CRLF.

Units we use are buffers and dos.

First we need a function to determine if there is a file or not in place.

 FUNCTION IsFile(FileName:STRING):Integer;
 

Will return 0 if file is found

VAR TSearchRec : SearchRec; BEGIN FindFirst(FileName,$3F,TSearchRec); IsFile:=DOSError; END;

That one is pretty straightforward, for more info take a look at DOSError in any good documentation about Pascal or DOS.

And we do also need a procedure (no errorchecking is implemented in it anyway since it would only be extra code to write) to get a line from a file and put it into a buffer.

 FUNCTION LoadFile(FileName:STRING):tBufferList;
 

And as always this performs no error-checking, you should make sure of that no error can occur before you call this one

VAR BuffList : tBufferList; Buffer : pBuffer; F : Text; S : STRING; BEGIN InitBufferList(BuffList); Assign(F,FileName); Reset(F); WHILE NOT EoF(F) DO BEGIN ReadLn(F,S); AllockBuffer(Buffer); AllocBufferData(Buffer,S); AddBufferToList(BuffList,Buffer); END; Close(F); LoadFile:=BuffList; END;

And a savefile is also needed, by now you should have gotten the hang of how I do things.. and as always, check for the file first, this procedure will overwrite the file if it exist.

 PROCEDURE SaveFile(FileName:STRING;BufferList:tBufferList);
 VAR F          : Text;
     S          : STRING;
     tempBuffer : pBuffer;
 BEGIN
    IF BufferList.FirstBuffer=NIL THEN Exit;
    Assign(F,FileName);
    tempBuffer:=BufferList.FirstBuffer;
    Rewrite(F);
    WHILE tempBuffer<>NIL DO BEGIN
       S:=GetBufferData(tempBuffer);
       WriteLn(F,S);
       tempBuffer:=tempBuffer^.Next;
    END;
    Close(F);   
 END;

And also that is the end of the actual tutorial.. now maybe you are thinking, "Hey, but this isn't an working editor, all I got was some lousy procedures, functions and units and nothing more". Well, that is true but hey, if you wanted me to write the entire program I would have written an GPL:ed editor, this shows one (and there are several more ways) so solve many of the problems that occur and can make you stuck for months until you give up completely.



These are popular related words: