/* -----------------------------------------------------------------------------

 ©1998 Dietmar Eilert. All Rights Reserved.

 Dietmar Eilert
 Mies-v-d-Rohe-Str.31, 52074 Aachen, Germany
 WWW: http://members.tripod.com/~golded
 E-Mail: dietmar@tomate.tng.oche.de
         Dietmar.Eilert@post.rwth-aachen.de
 Phone: +49-(0)241-81665 (German/English)
        +49-(0)2525-7776
 Fax:   +49-(0)241-81665 (9.00-18.30 GMT)

 Syntax parser example library. This is a context scanner highlighting ANSI C
 comments  and  C++  comments.  This  example  doesn't use a memory consuming
 syntax cache, ie. results of a prior parser pass are  not  stored.  However,
 the scanner will remember (cache) whether a line has been parsed and whether
 parts of a comment have been detected within the line using flag bits stored
 in the line's UserData slot.

 DICE:

 DMAKE

 -------------------------------------------------------------------------------

*/

#include "defs.h"

/// "Header stuff"

// Buffer handles are allocated for each text buffer to keep track of ressources:

struct BufferHandle {

    struct EditConfig     *bh_EditConfig;            // pointer to text data
    struct GlobalConfig   *bh_GlobalConfig;          // editor configuration
    struct SyntaxChunk    *bh_SyntaxStack;           // parser output
    struct RefreshRequest  bh_RefreshRequest;        // display refresh request
};

// these flag bits (attached to text lines) describe the line's properties

#define COMMENT_NONE      (1L<<0)                    //  1 no comment in this line
#define COMMENT_SIMPLE    (1L<<1)                    //  2 there is a comment in this line
#define COMMENT_START     (1L<<2)                    //  4 line is start of multi-line comment
#define COMMENT_BODY      (1L<<3)                    //  8 line is body  of multi-line comment
#define COMMENT_END       (1L<<4)                    // 16 line is end   of multi-line comment

///
/// "Prototype"

// library functions

Prototype LibCall struct ParserData     *MountScanner(void);
Prototype LibCall ULONG                  StartScanner(__A0 struct GlobalConfig *, __A1 struct EditConfig *, __D0 struct SyntaxChunk *, __D1 char *);
Prototype LibCall ULONG                  CloseScanner(__D0 ULONG);
Prototype LibCall void                   FlushScanner(__D0 ULONG);
Prototype LibCall void                   SetupScanner(__A0 struct GlobalConfig  *);
Prototype LibCall struct RefreshRequest *BriefScanner(__D0 ULONG, __A0 struct ScannerNotify *);
Prototype LibCall struct SyntaxChunk    *ParseLine   (__D0 ULONG, __A0 struct LineNode *, __D1 ULONG);
Prototype LibCall void                   UnparseLines(__A0 struct LineNode *, __D0 ULONG);
Prototype LibCall void                   ParseSection(__D0 ULONG, __A0 struct LineNode *, __D1 ULONG);

// private functions

Prototype struct LineNode       *FindContextStart(ULONG, struct LineNode *);
Prototype void                   ParseContext(struct LineNode *, ULONG);
Prototype ULONG                  ParseContextLine(struct LineNode *, ULONG);
Prototype struct SyntaxChunk    *ParseString(UBYTE *, UWORD, ULONG, ULONG);
Prototype BOOL                   BadSuccessor(ULONG, ULONG);
Prototype struct RefreshRequest *VerifyContext(ULONG, ULONG, ULONG);

///
/// "Library functions"

/* ------------------------------- MountScanner --------------------------------

 Called by the editor before first usage of a scanner. Return a description of
 our abilities.

*/

LibCall struct ParserData *
MountScanner()
{
    static UBYTE version[] = "$VER: Comments 2.2 (" __COMMODORE_DATE__ ")";

    static struct ParserData parserData;

    // syntax elements understood by parser

    static UBYTE *levelNames[] = {

        "Standard text",
        "ANSI Comment",
        "C++  Comment",
        NULL
    };

    static UBYTE *example[] = {

        "/* Simple ANSI C example */    ",
        "                               ",
        "#include <defs.h>              ",
        "                               ",
        "int                            ",
        "main(int argc, char **argv)    ",
        "{                              ",
        "   // anybody at home ?        ",
        "                               ",
        "   puts(\"Hello world !\");    ",
        "}                              ",

        NULL
    };

    // color suggestions

    static ULONG levelColors[] = {

        MAKE_RGB4(0,  0,   0),
        MAKE_RGB4(15, 15, 15),
        MAKE_RGB4(15, 15,  0)
    };

    parserData.pd_Release  = SCANLIBVERSION;
    parserData.pd_Version  = 1;
    parserData.pd_Serial   = 0;
    parserData.pd_Info     = "Comments 2.2";
    parserData.pd_Levels   = 3;
    parserData.pd_Names    = levelNames;
    parserData.pd_Colors   = levelColors;
    parserData.pd_Flags    = SCPRF_SYNTAXCACHE | SCPRF_CONTEXT;
    parserData.pd_Example  = example;

    return(&parserData);
}


/* ------------------------------- StartScanner --------------------------------

 Called by the editor after a new text buffer has been created. We allocate a
 buffer to hold text-specific data. The buffer address is returned as handle.
 Additionally, we preparse the unfolded text.

*/

LibCall ULONG
StartScanner(__A0 struct GlobalConfig *globalConfigPtr, __A1 struct EditConfig *editConfigPtr, __D0 struct SyntaxChunk *syntaxstack, __D1 char *arguments)
{
    struct BufferHandle *handle;

    if (handle = AllocVec(sizeof(struct BufferHandle), MEMF_PUBLIC | MEMF_CLEAR)) {

        handle->bh_GlobalConfig = globalConfigPtr;
        handle->bh_EditConfig   = editConfigPtr;
        handle->bh_SyntaxStack  = syntaxstack;
    }

    return((ULONG)handle);
}


/* ------------------------------- CloseScanner --------------------------------

 Called by the editor if a text buffer is about to be closed. Deallocate buffer
 specific 'global' data.

*/

LibCall ULONG
CloseScanner(__D0 ULONG scanID)
{
    if (scanID) {

        struct BufferHandle *handle = (struct BufferHandle *)scanID;

        FreeVec(handle);
    }

    return(0);
}


/* ------------------------------- FlushScanner --------------------------------

 Called by the editor in low memory situations: we are supposed to free as much
 memory as possible. This scanner doesn't use a memory consuming syntax cache,
 there is nothing to be freed.

*/

LibCall void
FlushScanner(__D0 ULONG scanID)
{
    ;
}


/* ------------------------------- SetupScanner --------------------------------

 Called by the editor if the user wants to change the scanner's configuration.
 We do not support user configuration (parserData.pd_Flags: SCPRF_CONFIGWIN flag
 unset).

*/

LibCall void
SetupScanner(__A0 struct GlobalConfig *globalConfigPtr)
{
    ;
}


/* ------------------------------- BriefScanner --------------------------------

 Called to notify a context scanner if lines have been added, deleted or
 modified. We are supposed to return an additional display request or NULL
 (the editor will refresh the damaged region only if we return NULL). Damaged
 lines have already been unparsed by the editor. This is what we have to do:

 a) lines have been deleted or modified

    Check whether the syntax scheme of the remaining lines is still valid (it
    will be invalid if say a comment start has been deleted but the comment
    end has not been deleted). Reparse the lines and return a refresh request
    if not.

 b) lines have been inserted

    Check whether the syntax sheme of the new lines affects the existing lines.
    Reparse all lines starting at the insertion point and return a refresh
    request if not.

 c) lines have been (un)folded

    This scanner assumes that folding doesn't affect syntax highlighting for the
    sake of simplicity. This isn't necessarily a valid assumption: we will run
    into touble if the user folds parts of a comment only (e.g. the first line).

*/

LibCall struct RefreshRequest *
BriefScanner(__D0 ULONG scanID, __A0 struct ScannerNotify *notify)
{
    struct EditConfig *config = ((struct BufferHandle *)scanID)->bh_EditConfig;

    switch (notify->sn_Class) {

        case SCANNER_NOTIFY_MODIFIED:

            ULONG line = notify->sn_Line;

            if (notify->sn_Removed > notify->sn_Lines) {

                // lines have been deleted

                return(VerifyContext(scanID, line, 0));
            }
            else if (notify->sn_Lines)

                // lines have been modified or inserted

                return(VerifyContext(scanID, line, notify->sn_Lines));
            else
                return(NULL);

            break;

        default:

            return(NULL);
    }
}


/* --------------------------------- ParseLine ---------------------------------

 Parse a line, build a syntax description

*/

LibCall struct SyntaxChunk *
ParseLine(__D0 ULONG scanID, __A0 struct LineNode *lineNode, __D1 ULONG line)
{
    if (IS_FOLD(lineNode))

        return(NULL);

    else if (lineNode->Len) {

        // not yet preparsed ? preparse a couple of lines

        if (lineNode->UserData == NULL) {

            ULONG lines = ((struct BufferHandle *)scanID)->bh_EditConfig->Lines - line;

            if (lines > 50)
                lines = 50;

            ParseSection(scanID, lineNode, lines);
        }

        if ((ULONG)lineNode->UserData & (COMMENT_SIMPLE | COMMENT_START | COMMENT_BODY | COMMENT_END))

            return(ParseString(lineNode->Text, lineNode->Len, scanID, (ULONG)lineNode->UserData));
        else
            return(NULL);
    }
    else
        return(NULL);
}

/* -------------------------------- UnparseLines -------------------------------

 Called by the editor if lines are to be deleted. We are supposed to free
 private data attached to the lines.

*/

LibCall void
UnparseLines(__A0  struct LineNode *lineNode, __D0 ULONG lines)
{
    while (lines--) {

        // free syntax cache

        lineNode->UserData = NULL;

        // free folded subblock

        if (IS_FOLD(lineNode)) {

            struct Fold *fold = (struct Fold *)lineNode->SpecialInfo;

            UnparseLines(fold->TextNodes, fold->Lines);
        }

        ++lineNode;
    }
}

/* -------------------------------- ParseSection -------------------------------

 Called by the editor if lines are to be displayed. The scanner is encouraged to
 preparse the lines.

*/

LibCall void
ParseSection(__D0 ULONG scanID, __A0  struct LineNode *lineNode, __D1 ULONG lines)
{
    struct LineNode *firstNode;

    if (firstNode = FindContextStart(scanID, lineNode))
        ParseContext(firstNode, lines + (lineNode - firstNode));
}


///
/// "private"

/*  ----------------------------- FindContextStart ------------------------------

 Search backwards until a parsed line or a context free line (fold or start of
 file) is found. Return line node. This node may be used as starting point for
 parsing lines.

*/

struct LineNode *
FindContextStart(scanID, lineNode)

ULONG            scanID;
struct LineNode *lineNode;
{
    ULONG line = lineNode - ((struct BufferHandle *)scanID)->bh_EditConfig-> TextNodes;

    while (line--) {

        // found a fold or a parsed line ?

        if (lineNode->UserData || IS_FOLD(lineNode))
            break;

        --lineNode;
    }

    return(lineNode);
}


/* ------------------------------- ParseContext --------------------------------

 Preparse lines. The first line is expected to be either a context free line
 (start of file or a fold header) or to be a parsed line.

*/

void
ParseContext(lineNode, lines)

struct LineNode *lineNode;
ULONG            lines;
{
    ULONG mode;

    for (mode = COMMENT_NONE; lines--; ++lineNode) {

        if (IS_FOLD(lineNode))

            mode = COMMENT_NONE;

        else {

            if (lineNode->UserData == NULL)
                lineNode->UserData = (APTR)ParseContextLine(lineNode, mode);

            mode = (ULONG)lineNode->UserData;
        }
    }
}


/* ----------------------------- ParseContextLine ------------------------------

 Preparse a line, ie. check whether there are comments. The status of the last
 line is passed in by the caller, this function will return the status (flag
 bits) of the new line.

*/

ULONG
ParseContextLine(lineNode, last)

struct LineNode *lineNode;
ULONG            last;
{
    UWORD len;
    ULONG status;

    if (last & (COMMENT_START | COMMENT_BODY))
        status = COMMENT_BODY;
    else
        status = FALSE;

    if (len = lineNode->Len) {

        BOOL   inString, inComment, commentStart;
        UBYTE *text;

        // preset status of this line if part of a multi-line comment

        inComment    = status & COMMENT_BODY;
        inString     = FALSE;
        commentStart = FALSE;

        for (text = lineNode->Text; len >= 2; ++text, --len) {

            if (inComment) {

                // end of comment detected ?

                if ((text[0] == '*') && (text[1] == '/')) {

                    // end of a muli-line comment or end of a simple comment ?

                    if (commentStart)

                        status |= COMMENT_SIMPLE;

                    else {

                        status |=  COMMENT_END;
                        status &= ~COMMENT_BODY;
                    }

                    inComment = FALSE;

                    ++text;
                    --len;
                }
            }
            else if (*text == 34)

                inString = !inString;

            else if (inString == FALSE) {

                // C++ comment detected (ignore rest of line) ?

                if ((text[0] == '/') && (text[1] == '/')) {

                    status |= COMMENT_SIMPLE;

                    break;
                }
                else if ((text[0] == '/') && (text[1] == '*')) {

                    // start of comment detected

                    inComment    = TRUE;
                    commentStart = TRUE;

                    ++text;
                    --len;
                }
            }
        }

        // comment not closed ?

        if (inComment) {

            // continuation of multi-line comment or new comment ?

            if (commentStart)
                status |= COMMENT_START;
            else
                status |= COMMENT_BODY;
        }
    }

    if (status == FALSE)
        status = COMMENT_NONE;

    return(status);
}


/* -------------------------------- ParseString --------------------------------

 Parse a string, build a syntax description. Return NULL if there is nothing to
 highlight.

*/

struct SyntaxChunk *
ParseString(UBYTE *text, UWORD len, ULONG scanID, ULONG status)
{
    if (len) {

        struct SyntaxChunk *syntaxStack = ((struct BufferHandle *)scanID)->bh_SyntaxStack;

        UWORD  element, indent, commentStart;
        BOOL   inComment;

        // leading spaces and tabs have to be ignored (we ignore all non-ascii characters)

        for (indent = 0; len && (*text <= 32); ++indent) {

            ++text;
            --len;
        }

        // trailing white space has to be ignored

        while (len && (text[len - 1] <= 32))

            --len;

        // no syntax elements found so far

        element = 0;

        // inner block line ?

        if (status & COMMENT_BODY) {

            inComment    = TRUE;
            commentStart = indent;
        }
        else {

            BOOL inString;

            if (inComment = (status & COMMENT_END))
                commentStart = indent;

            for (inString = FALSE; len >= 2; ++text, ++indent, --len) {

                if (inComment) {

                    // end of comment detected ?

                    if ((text[0] == '*') && (text[1] == '/')) {

                        syntaxStack[element].sc_Start = commentStart;
                        syntaxStack[element].sc_End   = indent + 1;
                        syntaxStack[element].sc_Level = 1;

                        inComment = FALSE;

                        ++element;
                        ++text;
                        ++indent;
                        --len;
                    }
                }
                else if (*text == 34)

                    inString = !inString;

                else if (inString == FALSE) {

                    // C++ comment detected (ignore rest of line) ?

                    if ((text[0] == '/') && (text[1] == '/')) {

                        syntaxStack[element].sc_Start = indent;
                        syntaxStack[element].sc_End   = indent + len - 1;
                        syntaxStack[element].sc_Level = 2;

                        ++element;

                        break;
                    }
                    else if ((text[0] == '/') && (text[1] == '*')) {

                        // start of comment detected

                        inComment    = TRUE;
                        commentStart = indent;

                        ++text;
                        ++indent;
                        --len;
                    }
                }
            }
        }

        // unterminated comment ? highlight rest of line

        if (inComment) {

            syntaxStack[element].sc_Start = commentStart;
            syntaxStack[element].sc_End   = indent + len - 1;
            syntaxStack[element].sc_Level = 1;

            ++element;
        }

        if (element) {

            // terminate syntax stack

            syntaxStack[element].sc_Start = FALSE;
            syntaxStack[element].sc_End   = FALSE;
            syntaxStack[element].sc_Level = FALSE;

            return(syntaxStack);
        }
        else
            return(NULL);

    }
    else
        return(NULL);
}


/* ------------------------------- VerifyContext -------------------------------

 Ensure that syntax information of a specified range of lines ("damage region")
 is valid and consistent with the existing text. Return a refresh request if
 not. The damage area is expected to have been unparsed already. This is what we
 have to do: we preparse existing lines before the damage area if belonging to
 the syntax context of the damage area (ie. all lines affecting highlighting of
 the first line in the damage area). The damage area is parsed, too. Parsed
 lines after the damage area are reparsed if highlighting is found to be
 inconsistent with the line(s) before. Reparsing continues until the end of the
 file respectively until no more inconsistencies are found.

*/

struct RefreshRequest *
VerifyContext(scanID, line, lines)

ULONG scanID, line, lines;
{
    struct EditConfig *config;
    struct LineNode   *lineNode, *lastNode;

    ULONG last, refreshStart, refresh = FALSE;

    config = ((struct BufferHandle *)scanID)->bh_EditConfig;

    lineNode = config->TextNodes + line;
    lastNode = lineNode + (lines - 1);

    // preparse from context start until end of damage area

    if (lines)
        ParseSection(scanID, lineNode, lines);

    // get syntax flags of last line in damage area

    if ((lastNode < config->TextNodes) || IS_FOLD(lastNode))
        last = COMMENT_NONE;
    else
        last = (ULONG)lastNode->UserData;

    // continue parsing until no more inconsistencies are found (until context end)

    refreshStart = (line += lines);

    for (lineNode = lastNode + 1; line < config->Lines; ++line, ++lineNode, ++refresh) {

        if (IS_FOLD(lineNode))

            break;

        else if (lineNode->UserData == NULL)

            lineNode->UserData = (APTR)ParseContextLine(lineNode, last);

        else if (BadSuccessor(last, (ULONG)lineNode->UserData))

            lineNode->UserData = (APTR)ParseContextLine(lineNode, last);
        else
            break;

        last = (ULONG)lineNode->UserData;
    }

    if (refresh) {

        struct RefreshRequest *refreshRequest = &((struct BufferHandle *)scanID)->bh_RefreshRequest;

        refreshRequest->rr_Line  = refreshStart;
        refreshRequest->rr_Lines = refresh;

        return(refreshRequest);
    }
    else
        return(NULL);
}


/* ------------------------------- BadSuccessor --------------------------------

 Return TRUE if syntax information of two adjacent lines is inconsistent.

*/

BOOL
BadSuccessor(pred, succ)

ULONG pred, succ;
{
    if (succ & (COMMENT_BODY | COMMENT_END)) {

        // comment body/end without start ?

        if ((pred & (COMMENT_START | COMMENT_BODY)) == FALSE)

            return(TRUE);
    }
    else if (pred & COMMENT_START) {

        // comment start without body/end ?

        if ((succ & (COMMENT_BODY | COMMENT_END)) == FALSE)

            return(TRUE);

        // comment inside comment ?

        if (succ & COMMENT_START)

            return(TRUE);
    }
    else if (pred & COMMENT_BODY) {

        // body line without end ?

        if ((succ & (COMMENT_BODY | COMMENT_END)) == FALSE)

            return(TRUE);
    }

    // C++ comment inside ANSI comment ?

    if (pred & (COMMENT_START | COMMENT_BODY))

        if (pred & COMMENT_SIMPLE)

            return(TRUE);

    return(FALSE);
}

///
