/*
   Editor syntax highlighting.

   Copyright (C) 1996-2024
   Free Software Foundation, Inc.

   Written by:
   Paul Sheer, 1998
   Leonard den Ottolander <leonard den ottolander nl>, 2005, 2006
   Egmont Koblinger <egmont@gmail.com>, 2010
   Slava Zanko <slavazanko@gmail.com>, 2013
   Andrew Borodin <aborodin@vmail.ru>, 2013, 2014, 2021

   This file is part of the Midnight Commander.

   The Midnight Commander is free software: you can redistribute it
   and/or modify it under the terms of the GNU General Public License as
   published by the Free Software Foundation, either version 3 of the License,
   or (at your option) any later version.

   The Midnight Commander is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/** \file
 *  \brief Source: editor syntax highlighting
 *  \author Paul Sheer
 *  \date 1996, 1997
 *  \author Mikhail Pobolovets
 *  \date 2010
 *
 *  Misspelled words are flushed from the syntax highlighting rules
 *  when they have been around longer than
 *  TRANSIENT_WORD_TIME_OUT seconds. At a cursor rate of 30
 *  chars per second and say 3 chars + a space per word, we can
 *  accumulate 450 words absolute max with a value of 60. This is
 *  below this limit of 1024 words in a context.
 */

#include <config.h>

#include <stdio.h>
#include <stdarg.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <sys/stat.h>
#include <stdlib.h>

#include "lib/global.h"
#include "lib/search.h"         /* search engine */
#include "lib/skin.h"
#include "lib/fileloc.h"        /* EDIT_SYNTAX_DIR, EDIT_SYNTAX_FILE */
#include "lib/strutil.h"        /* utf string functions */
#include "lib/util.h"
#include "lib/widget.h"         /* Listbox, message() */

#include "edit-impl.h"
#include "editwidget.h"

/*** global variables ****************************************************************************/

gboolean auto_syntax = TRUE;

/*** file scope macro definitions ****************************************************************/

/* bytes */
#define SYNTAX_MARKER_DENSITY 512

#define RULE_ON_LEFT_BORDER 1
#define RULE_ON_RIGHT_BORDER 2

#define SYNTAX_TOKEN_STAR       '\001'
#define SYNTAX_TOKEN_PLUS       '\002'
#define SYNTAX_TOKEN_BRACKET    '\003'
#define SYNTAX_TOKEN_BRACE      '\004'

#define break_a { result = line; break; }
#define check_a { if (*a == NULL) { result = line; break; } }
#define check_not_a { if (*a != NULL) { result = line ;break; } }

#define SYNTAX_KEYWORD(x) ((syntax_keyword_t *) (x))
#define CONTEXT_RULE(x) ((context_rule_t *) (x))

#define ARGS_LEN 1024

#define MAX_ENTRY_LEN 40
#define LIST_LINES 14
#define N_DFLT_ENTRIES 2

/*** file scope type declarations ****************************************************************/

typedef struct
{
    GString *keyword;
    char *whole_word_chars_left;
    char *whole_word_chars_right;
    gboolean line_start;
    int color;
} syntax_keyword_t;

typedef struct
{
    GString *left;
    unsigned char first_left;
    GString *right;
    unsigned char first_right;
    gboolean line_start_left;
    gboolean line_start_right;
    gboolean between_delimiters;
    char *whole_word_chars_left;
    char *whole_word_chars_right;
    char *keyword_first_chars;
    gboolean spelling;
    /* first word is word[1] */
    GPtrArray *keyword;
} context_rule_t;

typedef struct
{
    off_t offset;
    edit_syntax_rule_t rule;
} syntax_marker_t;

/*** forward declarations (file scope functions) *************************************************/

/*** file scope variables ************************************************************************/

static char *error_file_name = NULL;

/* --------------------------------------------------------------------------------------------- */
/*** file scope functions ************************************************************************/
/* --------------------------------------------------------------------------------------------- */

static void
syntax_keyword_free (gpointer keyword)
{
    syntax_keyword_t *k = SYNTAX_KEYWORD (keyword);

    g_string_free (k->keyword, TRUE);
    g_free (k->whole_word_chars_left);
    g_free (k->whole_word_chars_right);
    g_free (k);
}

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

static void
context_rule_free (gpointer rule)
{
    context_rule_t *r = CONTEXT_RULE (rule);

    g_string_free (r->left, TRUE);
    g_string_free (r->right, TRUE);
    g_free (r->whole_word_chars_left);
    g_free (r->whole_word_chars_right);
    g_free (r->keyword_first_chars);

    if (r->keyword != NULL)
        g_ptr_array_free (r->keyword, TRUE);

    g_free (r);
}

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

static gint
mc_defines_destroy (gpointer key, gpointer value, gpointer data)
{
    (void) data;

    g_free (key);
    g_strfreev ((char **) value);

    return FALSE;
}

/* --------------------------------------------------------------------------------------------- */
/** Completely destroys the defines tree */

static void
destroy_defines (GTree **defines)
{
    g_tree_foreach (*defines, mc_defines_destroy, NULL);
    g_tree_destroy (*defines);
    *defines = NULL;
}

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

/** Wrapper for case insensitive mode */
inline static int
xx_tolower (const WEdit *edit, int c)
{
    return edit->is_case_insensitive ? tolower (c) : c;
}

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

static void
subst_defines (GTree *defines, char **argv, char **argv_end)
{
    for (; *argv != NULL && argv < argv_end; argv++)
    {
        char **t;

        t = g_tree_lookup (defines, *argv);
        if (t != NULL)
        {
            int argc, count;
            char **p;

            /* Count argv array members */
            argc = g_strv_length (argv + 1);

            /* Count members of definition array */
            count = g_strv_length (t);

            p = argv + count + argc;
            /* Buffer overflow or infinitive loop in define */
            if (p >= argv_end)
                break;

            /* Move rest of argv after definition members */
            while (argc >= 0)
                *p-- = argv[argc-- + 1];

            /* Copy definition members to argv */
            for (p = argv; *t != NULL; *p++ = *t++)
                ;
        }
    }
}

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

static off_t
compare_word_to_right (const WEdit *edit, off_t i, const GString *text,
                       const char *whole_left, const char *whole_right, gboolean line_start)
{
    const unsigned char *p, *q;
    int c, d, j;

    c = edit_buffer_get_byte (&edit->buffer, i - 1);
    c = xx_tolower (edit, c);
    if ((line_start && c != '\n') || (whole_left != NULL && strchr (whole_left, c) != NULL))
        return -1;

    for (p = (const unsigned char *) text->str, q = p + text->len; p < q; p++, i++)
    {
        switch (*p)
        {
        case SYNTAX_TOKEN_STAR:
            if (++p > q)
                return -1;
            while (TRUE)
            {
                c = edit_buffer_get_byte (&edit->buffer, i);
                c = xx_tolower (edit, c);
                if (*p == '\0' && whole_right != NULL && strchr (whole_right, c) == NULL)
                    break;
                if (c == *p)
                    break;
                if (c == '\n')
                    return -1;
                i++;
            }
            break;
        case SYNTAX_TOKEN_PLUS:
            if (++p > q)
                return -1;
            j = 0;
            while (TRUE)
            {
                c = edit_buffer_get_byte (&edit->buffer, i);
                c = xx_tolower (edit, c);
                if (c == *p)
                {
                    j = i;
                    if (p[0] == text->str[0] && p[1] == '\0')   /* handle eg '+' and @+@ keywords properly */
                        break;
                }
                if (j != 0 && strchr ((const char *) p + 1, c) != NULL) /* c exists further down, so it will get matched later */
                    break;
                if (whiteness (c) || (whole_right != NULL && strchr (whole_right, c) == NULL))
                {
                    if (*p == '\0')
                    {
                        i--;
                        break;
                    }
                    if (j == 0)
                        return -1;
                    i = j;
                    break;
                }
                i++;
            }
            break;
        case SYNTAX_TOKEN_BRACKET:
            if (++p > q)
                return -1;
            c = -1;
            while (TRUE)
            {
                d = c;
                c = edit_buffer_get_byte (&edit->buffer, i);
                c = xx_tolower (edit, c);
                for (j = 0; p[j] != SYNTAX_TOKEN_BRACKET && p[j] != '\0'; j++)
                    if (c == p[j])
                        goto found_char2;
                break;
              found_char2:
                i++;
            }
            i--;
            while (*p != SYNTAX_TOKEN_BRACKET && p <= q)
                p++;
            if (p > q)
                return -1;
            if (p[1] == d)
                i--;
            break;
        case SYNTAX_TOKEN_BRACE:
            if (++p > q)
                return -1;
            c = edit_buffer_get_byte (&edit->buffer, i);
            c = xx_tolower (edit, c);
            for (; *p != SYNTAX_TOKEN_BRACE && *p != '\0'; p++)
                if (c == *p)
                    goto found_char3;
            return -1;
          found_char3:
            while (*p != SYNTAX_TOKEN_BRACE && p < q)
                p++;
            break;
        default:
            c = edit_buffer_get_byte (&edit->buffer, i);
            if (*p != xx_tolower (edit, c))
                return -1;
        }
    }

    if (whole_right == NULL)
        return i;

    c = edit_buffer_get_byte (&edit->buffer, i);
    c = xx_tolower (edit, c);
    return strchr (whole_right, c) != NULL ? -1 : i;
}

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

static const char *
xx_strchr (const WEdit *edit, const unsigned char *s, int char_byte)
{
    while (*s >= '\005' && xx_tolower (edit, *s) != char_byte)
        s++;

    return (const char *) s;
}

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

static void
apply_rules_going_right (WEdit *edit, off_t i)
{
    context_rule_t *r;
    int c;
    gboolean contextchanged = FALSE;
    gboolean found_left = FALSE, found_right = FALSE;
    gboolean keyword_foundleft = FALSE, keyword_foundright = FALSE;
    gboolean is_end;
    off_t end = 0;
    edit_syntax_rule_t _rule = edit->rule;

    c = edit_buffer_get_byte (&edit->buffer, i);
    c = xx_tolower (edit, c);
    if (c == 0)
        return;

    is_end = (edit->rule.end == i);

    /* check to turn off a keyword */
    if (_rule.keyword != 0)
    {
        if (edit_buffer_get_byte (&edit->buffer, i - 1) == '\n')
            _rule.keyword = 0;
        if (is_end)
        {
            _rule.keyword = 0;
            keyword_foundleft = TRUE;
        }
    }

    /* check to turn off a context */
    if (_rule.context != 0 && _rule.keyword == 0)
    {
        off_t e;

        r = CONTEXT_RULE (g_ptr_array_index (edit->rules, _rule.context));
        if (r->first_right == c && (edit->rule.border & RULE_ON_RIGHT_BORDER) == 0
            && r->right->len != 0 && (e =
                                      compare_word_to_right (edit, i, r->right,
                                                             r->whole_word_chars_left,
                                                             r->whole_word_chars_right,
                                                             r->line_start_right)) > 0)
        {
            _rule.end = e;
            found_right = TRUE;
            _rule.border = RULE_ON_RIGHT_BORDER;
            if (r->between_delimiters)
                _rule.context = 0;
        }
        else if (is_end && (edit->rule.border & RULE_ON_RIGHT_BORDER) != 0)
        {
            /* always turn off a context at 4 */
            found_left = TRUE;
            _rule.border = 0;
            if (!keyword_foundleft)
                _rule.context = 0;
        }
        else if (is_end && (edit->rule.border & RULE_ON_LEFT_BORDER) != 0)
        {
            /* never turn off a context at 2 */
            found_left = TRUE;
            _rule.border = 0;
        }
    }

    /* check to turn on a keyword */
    if (_rule.keyword == 0)
    {
        const char *p;

        r = CONTEXT_RULE (g_ptr_array_index (edit->rules, _rule.context));
        p = r->keyword_first_chars;

        if (p != NULL)
            while (*(p = xx_strchr (edit, (const unsigned char *) p + 1, c)) != '\0')
            {
                syntax_keyword_t *k;
                int count;
                off_t e = -1;

                count = p - r->keyword_first_chars;
                k = SYNTAX_KEYWORD (g_ptr_array_index (r->keyword, count));
                if (k->keyword != 0)
                    e = compare_word_to_right (edit, i, k->keyword, k->whole_word_chars_left,
                                               k->whole_word_chars_right, k->line_start);
                if (e > 0)
                {
                    /* when both context and keyword terminate with a newline,
                       the context overflows to the next line and colorizes it incorrectly */
                    if (e > i + 1 && _rule._context != 0
                        && k->keyword->str[k->keyword->len - 1] == '\n')
                    {
                        r = CONTEXT_RULE (g_ptr_array_index (edit->rules, _rule._context));
                        if (r->right != NULL && r->right->len != 0
                            && r->right->str[r->right->len - 1] == '\n')
                            e--;
                    }

                    end = e;
                    _rule.end = e;
                    _rule.keyword = count;
                    keyword_foundright = TRUE;
                    break;
                }
            }
    }

    /* check to turn on a context */
    if (_rule.context == 0)
    {
        if (!found_left && is_end)
        {
            if ((edit->rule.border & RULE_ON_RIGHT_BORDER) != 0)
            {
                _rule.border = 0;
                _rule.context = 0;
                contextchanged = TRUE;
                _rule.keyword = 0;

            }
            else if ((edit->rule.border & RULE_ON_LEFT_BORDER) != 0)
            {
                r = CONTEXT_RULE (g_ptr_array_index (edit->rules, _rule._context));
                _rule.border = 0;
                if (r->between_delimiters)
                {
                    _rule.context = _rule._context;
                    contextchanged = TRUE;
                    _rule.keyword = 0;

                    if (r->first_right == c)
                    {
                        off_t e = -1;

                        if (r->right->len != 0)
                            e = compare_word_to_right (edit, i, r->right, r->whole_word_chars_left,
                                                       r->whole_word_chars_right,
                                                       r->line_start_right);
                        if (e >= end)
                        {
                            _rule.end = e;
                            found_right = TRUE;
                            _rule.border = RULE_ON_RIGHT_BORDER;
                            _rule.context = 0;
                        }
                    }
                }
            }
        }

        if (!found_right)
        {
            size_t count;

            for (count = 1; count < edit->rules->len; count++)
            {
                r = CONTEXT_RULE (g_ptr_array_index (edit->rules, count));
                if (r->first_left == c)
                {
                    off_t e = -1;

                    if (r->left->len != 0)
                        e = compare_word_to_right (edit, i, r->left, r->whole_word_chars_left,
                                                   r->whole_word_chars_right, r->line_start_left);
                    if (e >= end && (_rule.keyword == 0 || keyword_foundright))
                    {
                        _rule.end = e;
                        _rule.border = RULE_ON_LEFT_BORDER;
                        _rule._context = count;
                        if (!r->between_delimiters && _rule.keyword == 0)
                        {
                            _rule.context = count;
                            contextchanged = TRUE;
                        }
                        break;
                    }
                }
            }
        }
    }

    /* check again to turn on a keyword if the context switched */
    if (contextchanged && _rule.keyword == 0)
    {
        const char *p;

        r = CONTEXT_RULE (g_ptr_array_index (edit->rules, _rule.context));
        p = r->keyword_first_chars;

        while (*(p = xx_strchr (edit, (const unsigned char *) p + 1, c)) != '\0')
        {
            syntax_keyword_t *k;
            int count;
            off_t e = -1;

            count = p - r->keyword_first_chars;
            k = SYNTAX_KEYWORD (g_ptr_array_index (r->keyword, count));

            if (k->keyword->len != 0)
                e = compare_word_to_right (edit, i, k->keyword, k->whole_word_chars_left,
                                           k->whole_word_chars_right, k->line_start);
            if (e > 0)
            {
                _rule.end = e;
                _rule.keyword = count;
                break;
            }
        }
    }

    edit->rule = _rule;
}

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

static void
edit_get_rule (WEdit *edit, off_t byte_index)
{
    off_t i;

    if (byte_index > edit->last_get_rule)
    {
        for (i = edit->last_get_rule + 1; i <= byte_index; i++)
        {
            off_t d = SYNTAX_MARKER_DENSITY;

            apply_rules_going_right (edit, i);

            if (edit->syntax_marker != NULL)
                d += ((syntax_marker_t *) edit->syntax_marker->data)->offset;

            if (i > d)
            {
                syntax_marker_t *s;

                s = g_new (syntax_marker_t, 1);
                s->offset = i;
                s->rule = edit->rule;
                edit->syntax_marker = g_slist_prepend (edit->syntax_marker, s);
            }
        }
    }
    else if (byte_index < edit->last_get_rule)
    {
        while (TRUE)
        {
            syntax_marker_t *s;

            if (edit->syntax_marker == NULL)
            {
                memset (&edit->rule, 0, sizeof (edit->rule));
                for (i = -1; i <= byte_index; i++)
                    apply_rules_going_right (edit, i);
                break;
            }

            s = (syntax_marker_t *) edit->syntax_marker->data;

            if (byte_index >= s->offset)
            {
                edit->rule = s->rule;
                for (i = s->offset + 1; i <= byte_index; i++)
                    apply_rules_going_right (edit, i);
                break;
            }

            g_free (s);
            edit->syntax_marker = g_slist_delete_link (edit->syntax_marker, edit->syntax_marker);
        }
    }
    edit->last_get_rule = byte_index;
}

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

static int
translate_rule_to_color (const WEdit *edit, const edit_syntax_rule_t *rule)
{
    syntax_keyword_t *k;
    context_rule_t *r;

    r = CONTEXT_RULE (g_ptr_array_index (edit->rules, rule->context));
    k = SYNTAX_KEYWORD (g_ptr_array_index (r->keyword, rule->keyword));

    return k->color;
}

/* --------------------------------------------------------------------------------------------- */
/**
   Returns 0 on error/eof or a count of the number of bytes read
   including the newline. Result must be free'd.
   In case of an error, *line will not be modified.
 */

static size_t
read_one_line (char **line, FILE *f)
{
    GString *p;
    size_t r = 0;

    /* not reallocate string too often */
    p = g_string_sized_new (64);

    while (TRUE)
    {
        int c;

        c = fgetc (f);
        if (c == EOF)
        {
            if (ferror (f))
            {
                if (errno == EINTR)
                    continue;
                r = 0;
            }
            break;
        }
        r++;

        /* handle all of \r\n, \r, \n correctly. */
        if (c == '\n')
            break;
        if (c == '\r')
        {
            c = fgetc (f);
            if (c == '\n')
                r++;
            else
                ungetc (c, f);
            break;
        }

        g_string_append_c (p, c);
    }
    if (r != 0)
        *line = g_string_free (p, FALSE);
    else
        g_string_free (p, TRUE);

    return r;
}

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

static char *
convert (char *s)
{
    char *r, *p;

    p = r = s;
    while (*s)
    {
        switch (*s)
        {
        case '\\':
            s++;
            switch (*s)
            {
            case ' ':
                *p = ' ';
                s--;
                break;
            case 'n':
                *p = '\n';
                break;
            case 'r':
                *p = '\r';
                break;
            case 't':
                *p = '\t';
                break;
            case 's':
                *p = ' ';
                break;
            case '*':
                *p = '*';
                break;
            case '\\':
                *p = '\\';
                break;
            case '[':
            case ']':
                *p = SYNTAX_TOKEN_BRACKET;
                break;
            case '{':
            case '}':
                *p = SYNTAX_TOKEN_BRACE;
                break;
            case 0:
                *p = *s;
                return r;
            default:
                *p = *s;
                break;
            }
            break;
        case '*':
            *p = SYNTAX_TOKEN_STAR;
            break;
        case '+':
            *p = SYNTAX_TOKEN_PLUS;
            break;
        default:
            *p = *s;
            break;
        }
        s++;
        p++;
    }
    *p = '\0';
    return r;
}

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

static int
get_args (char *l, char **args, int args_size)
{
    int argc = 0;

    while (argc < args_size)
    {
        char *p = l;

        while (*p != '\0' && whiteness (*p))
            p++;
        if (*p == '\0')
            break;
        for (l = p + 1; *l != '\0' && !whiteness (*l); l++)
            ;
        if (*l != '\0')
            *l++ = '\0';
        args[argc++] = convert (p);
    }
    args[argc] = (char *) NULL;
    return argc;
}

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

static int
this_try_alloc_color_pair (tty_color_pair_t *color)
{
    char f[80], b[80], a[80], *p;

    if (color->bg != NULL && *color->bg == '\0')
        color->bg = NULL;
    if (color->fg != NULL && *color->fg == '\0')
        color->fg = NULL;
    if (color->attrs != NULL && *color->attrs == '\0')
        color->attrs = NULL;

    if (color->fg == NULL && color->bg == NULL)
        return EDITOR_NORMAL_COLOR;

    if (color->fg != NULL)
    {
        g_strlcpy (f, color->fg, sizeof (f));
        p = strchr (f, '/');
        if (p != NULL)
            *p = '\0';
        color->fg = f;
    }
    if (color->bg != NULL)
    {
        g_strlcpy (b, color->bg, sizeof (b));
        p = strchr (b, '/');
        if (p != NULL)
            *p = '\0';
        color->bg = b;
    }
    if (color->fg == NULL || color->bg == NULL)
    {
        /* get colors from skin */
        char *editnormal;

        editnormal = mc_skin_get ("editor", "_default_", "default;default");

        if (color->fg == NULL)
        {
            g_strlcpy (f, editnormal, sizeof (f));
            p = strchr (f, ';');
            if (p != NULL)
                *p = '\0';
            if (f[0] == '\0')
                g_strlcpy (f, "default", sizeof (f));
            color->fg = f;
        }
        if (color->bg == NULL)
        {
            p = strchr (editnormal, ';');
            if ((p != NULL) && (*(++p) != '\0'))
                g_strlcpy (b, p, sizeof (b));
            else
                g_strlcpy (b, "default", sizeof (b));
            color->bg = b;
        }

        g_free (editnormal);
    }

    if (color->attrs != NULL)
    {
        g_strlcpy (a, color->attrs, sizeof (a));
        p = strchr (a, '/');
        if (p != NULL)
            *p = '\0';
        /* get_args() mangles the + signs, unmangle 'em */
        p = a;
        while ((p = strchr (p, SYNTAX_TOKEN_PLUS)) != NULL)
            *p++ = '+';
        color->attrs = a;
    }

    return tty_try_alloc_color_pair (color, TRUE);
}

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

static FILE *
open_include_file (const char *filename)
{
    FILE *f;

    g_free (error_file_name);
    error_file_name = g_strdup (filename);
    if (g_path_is_absolute (filename))
        return fopen (filename, "r");

    g_free (error_file_name);
    error_file_name =
        g_build_filename (mc_config_get_data_path (), EDIT_SYNTAX_DIR, filename, (char *) NULL);
    f = fopen (error_file_name, "r");
    if (f != NULL)
        return f;

    g_free (error_file_name);
    error_file_name =
        g_build_filename (mc_global.share_data_dir, EDIT_SYNTAX_DIR, filename, (char *) NULL);

    return fopen (error_file_name, "r");
}

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

inline static void
xx_lowerize_line (WEdit *edit, char *line, size_t len)
{
    if (edit->is_case_insensitive)
    {
        size_t i;

        for (i = 0; i < len; ++i)
            line[i] = tolower (line[i]);
    }
}

/* --------------------------------------------------------------------------------------------- */
/** returns line number on error */

static int
edit_read_syntax_rules (WEdit *edit, FILE *f, char **args, int args_size)
{
    FILE *g = NULL;
    tty_color_pair_t color;
    char last_fg[32] = "", last_bg[32] = "", last_attrs[64] = "";
    char whole_right[512];
    char whole_left[512];
    char *l = NULL;
    int save_line = 0, line = 0;
    context_rule_t *c = NULL;
    gboolean no_words = TRUE;
    int result = 0;

    args[0] = NULL;
    edit->is_case_insensitive = FALSE;

    strcpy (whole_left, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_01234567890");
    strcpy (whole_right, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_01234567890");

    edit->rules = g_ptr_array_new_with_free_func (context_rule_free);

    if (edit->defines == NULL)
        edit->defines = g_tree_new ((GCompareFunc) strcmp);

    while (TRUE)
    {
        char **a;
        size_t len;
        int argc;

        line++;
        l = NULL;

        len = read_one_line (&l, f);
        if (len == 0)
        {
            if (g == NULL)
                break;

            fclose (f);
            f = g;
            g = NULL;
            line = save_line + 1;
            MC_PTR_FREE (error_file_name);
            MC_PTR_FREE (l);
            len = read_one_line (&l, f);
            if (len == 0)
                break;
        }

        xx_lowerize_line (edit, l, len);

        argc = get_args (l, args, args_size);
        a = args + 1;
        if (args[0] == NULL)
        {
            /* do nothing */
        }
        else if (strcmp (args[0], "include") == 0)
        {
            if (g != NULL || argc != 2)
            {
                result = line;
                break;
            }
            g = f;
            f = open_include_file (args[1]);
            if (f == NULL)
            {
                MC_PTR_FREE (error_file_name);
                result = line;
                break;
            }
            save_line = line;
            line = 0;
        }
        else if (strcmp (args[0], "caseinsensitive") == 0)
        {
            edit->is_case_insensitive = TRUE;
        }
        else if (strcmp (args[0], "wholechars") == 0)
        {
            check_a;
            if (strcmp (*a, "left") == 0)
            {
                a++;
                g_strlcpy (whole_left, *a, sizeof (whole_left));
            }
            else if (strcmp (*a, "right") == 0)
            {
                a++;
                g_strlcpy (whole_right, *a, sizeof (whole_right));
            }
            else
            {
                g_strlcpy (whole_left, *a, sizeof (whole_left));
                g_strlcpy (whole_right, *a, sizeof (whole_right));
            }
            a++;
            check_not_a;
        }
        else if (strcmp (args[0], "context") == 0)
        {
            syntax_keyword_t *k;

            check_a;
            if (edit->rules->len == 0)
            {
                /* first context is the default */
                if (strcmp (*a, "default") != 0)
                    break_a;

                a++;
                c = g_new0 (context_rule_t, 1);
                g_ptr_array_add (edit->rules, c);
                c->left = g_string_new (" ");
                c->right = g_string_new (" ");
            }
            else
            {
                /* Start new context.  */
                c = g_new0 (context_rule_t, 1);
                g_ptr_array_add (edit->rules, c);
                if (strcmp (*a, "exclusive") == 0)
                {
                    a++;
                    c->between_delimiters = TRUE;
                }
                check_a;
                if (strcmp (*a, "whole") == 0)
                {
                    a++;
                    c->whole_word_chars_left = g_strdup (whole_left);
                    c->whole_word_chars_right = g_strdup (whole_right);
                }
                else if (strcmp (*a, "wholeleft") == 0)
                {
                    a++;
                    c->whole_word_chars_left = g_strdup (whole_left);
                }
                else if (strcmp (*a, "wholeright") == 0)
                {
                    a++;
                    c->whole_word_chars_right = g_strdup (whole_right);
                }
                check_a;
                if (strcmp (*a, "linestart") == 0)
                {
                    a++;
                    c->line_start_left = TRUE;
                }
                check_a;
                c->left = g_string_new (*a++);
                check_a;
                if (strcmp (*a, "linestart") == 0)
                {
                    a++;
                    c->line_start_right = TRUE;
                }
                check_a;
                c->right = g_string_new (*a++);
                c->first_left = c->left->str[0];
                c->first_right = c->right->str[0];
            }
            c->keyword = g_ptr_array_new_with_free_func (syntax_keyword_free);
            k = g_new0 (syntax_keyword_t, 1);
            g_ptr_array_add (c->keyword, k);
            no_words = FALSE;
            subst_defines (edit->defines, a, &args[ARGS_LEN]);
            color.fg = *a;
            if (*a != NULL)
                a++;
            color.bg = *a;
            if (*a != NULL)
                a++;
            color.attrs = *a;
            if (*a != NULL)
                a++;
            g_strlcpy (last_fg, color.fg != NULL ? color.fg : "", sizeof (last_fg));
            g_strlcpy (last_bg, color.bg != NULL ? color.bg : "", sizeof (last_bg));
            g_strlcpy (last_attrs, color.attrs != NULL ? color.attrs : "", sizeof (last_attrs));
            k->color = this_try_alloc_color_pair (&color);
            k->keyword = g_string_new (" ");
            check_not_a;
        }
        else if (strcmp (args[0], "spellcheck") == 0)
        {
            if (c == NULL)
            {
                result = line;
                break;
            }
            c->spelling = TRUE;
        }
        else if (strcmp (args[0], "keyword") == 0)
        {
            context_rule_t *last_rule;
            syntax_keyword_t *k;

            if (no_words)
                break_a;
            check_a;
            last_rule = CONTEXT_RULE (g_ptr_array_index (edit->rules, edit->rules->len - 1));
            k = g_new0 (syntax_keyword_t, 1);
            g_ptr_array_add (last_rule->keyword, k);
            if (strcmp (*a, "whole") == 0)
            {
                a++;
                k->whole_word_chars_left = g_strdup (whole_left);
                k->whole_word_chars_right = g_strdup (whole_right);
            }
            else if (strcmp (*a, "wholeleft") == 0)
            {
                a++;
                k->whole_word_chars_left = g_strdup (whole_left);
            }
            else if (strcmp (*a, "wholeright") == 0)
            {
                a++;
                k->whole_word_chars_right = g_strdup (whole_right);
            }
            check_a;
            if (strcmp (*a, "linestart") == 0)
            {
                a++;
                k->line_start = TRUE;
            }
            check_a;
            if (strcmp (*a, "whole") == 0)
                break_a;

            k->keyword = g_string_new (*a++);
            subst_defines (edit->defines, a, &args[ARGS_LEN]);
            color.fg = *a;
            if (*a != NULL)
                a++;
            color.bg = *a;
            if (*a != NULL)
                a++;
            color.attrs = *a;
            if (*a != NULL)
                a++;
            if (color.fg == NULL)
                color.fg = last_fg;
            if (color.bg == NULL)
                color.bg = last_bg;
            if (color.attrs == NULL)
                color.attrs = last_attrs;
            k->color = this_try_alloc_color_pair (&color);
            check_not_a;
        }
        else if (*(args[0]) == '#')
        {
            /* do nothing for comment */
        }
        else if (strcmp (args[0], "file") == 0)
        {
            break;
        }
        else if (strcmp (args[0], "define") == 0)
        {
            char *key = *a++;
            char **argv;

            if (argc < 3)
                break_a;
            argv = g_tree_lookup (edit->defines, key);
            if (argv != NULL)
                mc_defines_destroy (NULL, argv, NULL);
            else
                key = g_strdup (key);

            argv = g_new (char *, argc - 1);
            g_tree_insert (edit->defines, key, argv);
            while (*a != NULL)
                *argv++ = g_strdup (*a++);
            *argv = NULL;
        }
        else
        {
            /* anything else is an error */
            break_a;
        }
        MC_PTR_FREE (l);
    }
    MC_PTR_FREE (l);

    if (edit->rules->len == 0)
    {
        g_ptr_array_free (edit->rules, TRUE);
        edit->rules = NULL;
    }

    if (result == 0)
    {
        size_t i;
        GString *first_chars;

        if (edit->rules == NULL)
            return line;

        first_chars = g_string_sized_new (32);

        /* collect first character of keywords */
        for (i = 0; i < edit->rules->len; i++)
        {
            size_t j;

            g_string_set_size (first_chars, 0);
            c = CONTEXT_RULE (g_ptr_array_index (edit->rules, i));

            g_string_append_c (first_chars, (char) 1);
            for (j = 1; j < c->keyword->len; j++)
            {
                syntax_keyword_t *k;

                k = SYNTAX_KEYWORD (g_ptr_array_index (c->keyword, j));
                g_string_append_c (first_chars, k->keyword->str[0]);
            }

            c->keyword_first_chars = g_strndup (first_chars->str, first_chars->len);
        }

        g_string_free (first_chars, TRUE);
    }

    return result;
}

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

/* returns -1 on file error, line number on error in file syntax */
static int
edit_read_syntax_file (WEdit *edit, GPtrArray *pnames, const char *syntax_file,
                       const char *editor_file, const char *first_line, const char *type)
{
    FILE *f, *g = NULL;
    char *args[ARGS_LEN], *l = NULL;
    long line = 0;
    int result = 0;
    gboolean found = FALSE;

    f = fopen (syntax_file, "r");
    if (f == NULL)
    {
        char *global_syntax_file;

        global_syntax_file =
            g_build_filename (mc_global.share_data_dir, EDIT_SYNTAX_FILE, (char *) NULL);
        f = fopen (global_syntax_file, "r");
        g_free (global_syntax_file);
        if (f == NULL)
            return -1;
    }

    args[0] = NULL;
    while (TRUE)
    {
        line++;
        MC_PTR_FREE (l);
        if (read_one_line (&l, f) == 0)
            break;
        (void) get_args (l, args, ARGS_LEN - 1);        /* Final NULL */
        if (args[0] == NULL)
            continue;

        /* Looking for 'include ...' lines before first 'file ...' ones */
        if (!found && strcmp (args[0], "include") == 0)
        {
            if (args[1] == NULL || (g = open_include_file (args[1])) == NULL)
            {
                result = line;
                break;
            }
            goto found_type;
        }

        /* looking for 'file ...' lines only */
        if (strcmp (args[0], "file") != 0)
            continue;

        found = TRUE;

        /* must have two args or report error */
        if (args[1] == NULL || args[2] == NULL)
        {
            result = line;
            break;
        }

        if (pnames != NULL)
        {
            /* 1: just collecting a list of names of rule sets */
            g_ptr_array_add (pnames, g_strdup (args[2]));
        }
        else if (type != NULL)
        {
            /* 2: rule set was explicitly specified by the caller */
            if (strcmp (type, args[2]) == 0)
                goto found_type;
        }
        else if (editor_file != NULL && edit != NULL)
        {
            /* 3: auto-detect rule set from regular expressions */
            gboolean q;

            q = mc_search (args[1], DEFAULT_CHARSET, editor_file, MC_SEARCH_T_REGEX);
            /* does filename match arg 1 ? */
            if (!q && args[3] != NULL)
            {
                /* does first line match arg 3 ? */
                q = mc_search (args[3], DEFAULT_CHARSET, first_line, MC_SEARCH_T_REGEX);
            }
            if (q)
            {
                int line_error;
                char *syntax_type;

              found_type:
                syntax_type = args[2];
                line_error = edit_read_syntax_rules (edit, g ? g : f, args, ARGS_LEN - 1);
                if (line_error != 0)
                {
                    if (error_file_name == NULL)        /* an included file */
                        result = line + line_error;
                    else
                        result = line_error;
                }
                else
                {
                    g_free (edit->syntax_type);
                    edit->syntax_type = g_strdup (syntax_type);
                    /* if there are no rules then turn off syntax highlighting for speed */
                    if (g == NULL && edit->rules->len == 1)
                    {
                        context_rule_t *r0;

                        r0 = CONTEXT_RULE (g_ptr_array_index (edit->rules, 0));
                        if (r0->keyword->len == 1 && !r0->spelling)
                        {
                            edit_free_syntax_rules (edit);
                            break;
                        }
                    }
                }

                if (g == NULL)
                    break;

                fclose (g);
                g = NULL;
            }
        }
    }
    g_free (l);
    fclose (f);
    return result;
}

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

static const char *
get_first_editor_line (WEdit *edit)
{
    static char s[256];

    s[0] = '\0';

    if (edit != NULL)
    {
        size_t i;

        for (i = 0; i < sizeof (s) - 1; i++)
        {
            s[i] = edit_buffer_get_byte (&edit->buffer, i);
            if (s[i] == '\n')
            {
                s[i] = '\0';
                break;
            }
        }

        s[sizeof (s) - 1] = '\0';
    }

    return s;
}

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

static int
pstrcmp (const void *p1, const void *p2)
{
    return strcmp (*(char *const *) p1, *(char *const *) p2);
}

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

static int
exec_edit_syntax_dialog (const GPtrArray *names, const char *current_syntax)
{
    size_t i;
    Listbox *syntaxlist;

    syntaxlist = listbox_window_new (LIST_LINES, MAX_ENTRY_LEN,
                                     _("Choose syntax highlighting"), NULL);
    LISTBOX_APPEND_TEXT (syntaxlist, 'A', _("< Auto >"), NULL, FALSE);
    LISTBOX_APPEND_TEXT (syntaxlist, 'R', _("< Reload Current Syntax >"), NULL, FALSE);

    for (i = 0; i < names->len; i++)
    {
        const char *name;

        name = g_ptr_array_index (names, i);
        LISTBOX_APPEND_TEXT (syntaxlist, 0, name, NULL, FALSE);
        if (current_syntax != NULL && strcmp (name, current_syntax) == 0)
            listbox_set_current (syntaxlist->list, i + N_DFLT_ENTRIES);
    }

    return listbox_run (syntaxlist);
}

/* --------------------------------------------------------------------------------------------- */
/*** public functions ****************************************************************************/
/* --------------------------------------------------------------------------------------------- */

int
edit_get_syntax_color (WEdit *edit, off_t byte_index)
{
    if (!tty_use_colors ())
        return 0;

    if (edit_options.syntax_highlighting && edit->rules != NULL && byte_index < edit->buffer.size)
    {
        edit_get_rule (edit, byte_index);
        return translate_rule_to_color (edit, &edit->rule);
    }

    return EDITOR_NORMAL_COLOR;
}

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

void
edit_free_syntax_rules (WEdit *edit)
{
    if (edit == NULL)
        return;

    if (edit->defines != NULL)
        destroy_defines (&edit->defines);

    if (edit->rules == NULL)
        return;

    edit_get_rule (edit, -1);
    MC_PTR_FREE (edit->syntax_type);

    g_ptr_array_free (edit->rules, TRUE);
    edit->rules = NULL;
    g_clear_slist (&edit->syntax_marker, g_free);
    tty_color_free_temp ();
}

/* --------------------------------------------------------------------------------------------- */
/**
 * Load rules into edit struct.  Either edit or *pnames must be NULL.  If
 * edit is NULL, a list of types will be stored into names.  If type is
 * NULL, then the type will be selected according to the filename.
 * type must be edit->syntax_type or NULL
 */
void
edit_load_syntax (WEdit *edit, GPtrArray *pnames, const char *type)
{
    int r;
    char *f = NULL;

    if (auto_syntax)
        type = NULL;

    if (edit != NULL)
    {
        char *saved_type;

        saved_type = g_strdup (type);   /* save edit->syntax_type */
        edit_free_syntax_rules (edit);
        edit->syntax_type = saved_type; /* restore edit->syntax_type */
    }

    if (!tty_use_colors ())
        return;

    if (!edit_options.syntax_highlighting && (pnames == NULL || pnames->len == 0))
        return;

    if (edit != NULL && edit->filename_vpath == NULL)
        return;

    f = mc_config_get_full_path (EDIT_SYNTAX_FILE);
    if (edit != NULL)
        r = edit_read_syntax_file (edit, pnames, f, vfs_path_as_str (edit->filename_vpath),
                                   get_first_editor_line (edit),
                                   auto_syntax ? NULL : edit->syntax_type);
    else
        r = edit_read_syntax_file (NULL, pnames, f, NULL, "", NULL);
    if (r == -1)
    {
        edit_free_syntax_rules (edit);
        message (D_ERROR, _("Load syntax file"),
                 _("Cannot open file %s\n%s"), f, unix_error_string (errno));
    }
    else if (r != 0)
    {
        edit_free_syntax_rules (edit);
        message (D_ERROR, _("Load syntax file"),
                 _("Error in file %s on line %d"), error_file_name != NULL ? error_file_name : f,
                 r);
        MC_PTR_FREE (error_file_name);
    }

    g_free (f);
}

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

const char *
edit_get_syntax_type (const WEdit *edit)
{
    return edit->syntax_type;
}

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

void
edit_syntax_dialog (WEdit *edit)
{
    GPtrArray *names;
    int syntax;

    names = g_ptr_array_new_with_free_func (g_free);

    /* We fill the list of syntax files every time the editor is invoked.
       Instead we could save the list to a file and update it once the syntax
       file gets updated (either by testing or by explicit user command). */
    edit_load_syntax (NULL, names, NULL);
    g_ptr_array_sort (names, pstrcmp);

    syntax = exec_edit_syntax_dialog (names, edit->syntax_type);
    if (syntax >= 0)
    {
        gboolean force_reload = FALSE;
        char *current_syntax;
        gboolean old_auto_syntax;

        current_syntax = g_strdup (edit->syntax_type);
        old_auto_syntax = auto_syntax;

        switch (syntax)
        {
        case 0:                /* auto syntax */
            auto_syntax = TRUE;
            break;
        case 1:                /* reload current syntax */
            force_reload = TRUE;
            break;
        default:
            auto_syntax = FALSE;
            g_free (edit->syntax_type);
            edit->syntax_type = g_strdup (g_ptr_array_index (names, syntax - N_DFLT_ENTRIES));
        }

        /* Load or unload syntax rules if the option has changed */
        if (force_reload || (auto_syntax && !old_auto_syntax) || old_auto_syntax ||
            (current_syntax != NULL && edit->syntax_type != NULL &&
             strcmp (current_syntax, edit->syntax_type) != 0))
            edit_load_syntax (edit, NULL, edit->syntax_type);

        g_free (current_syntax);
    }

    g_ptr_array_free (names, TRUE);
}

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